code examples

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

Send and Receive SMS and WhatsApp Messages with Node.js and Vonage Messages API

Learn how to build a Node.js Express application to send and receive SMS and WhatsApp messages using the Vonage Messages API. Complete guide with webhooks, security, and deployment.

Send and Receive SMS and WhatsApp Messages with Node.js and Vonage Messages API

Build a Node.js application using Express to send and receive SMS and WhatsApp messages via the Vonage Messages API. This guide covers project setup, configuration, webhook handling, security implementation, and deployment preparation.

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

  • Sending SMS messages programmatically.
  • Sending WhatsApp messages using the Vonage WhatsApp Sandbox for testing.
  • Receiving incoming SMS and WhatsApp messages via webhooks.
  • Verifying incoming webhook requests for security.
  • Handling basic errors and logging essential information.

This enables your application to communicate with users via SMS and WhatsApp for notifications, alerts, two-factor authentication (2FA), customer support, and more – all through a unified API interface.

Technologies used

  • Node.js: JavaScript runtime environment for building server-side applications.
  • Express: Minimal and flexible Node.js web application framework.
  • Vonage Messages API: Unified API for sending and receiving messages across multiple channels (SMS, MMS, WhatsApp, Facebook Messenger, Viber).
  • Vonage Node SDK: Simplifies interaction with Vonage APIs in Node.js. This guide uses these SDK modules:
    • @vonage/server-sdk: Core client initialization and authentication.
    • @vonage/messages: Constructing and sending messages via the Messages API.
    • @vonage/jwt: Verifying signatures of incoming webhooks.
  • ngrok: Tool to expose local development servers to the internet for webhook testing.
  • dotenv: Module to load environment variables from a .env file.

Prerequisites

Before you start, ensure you have:

  1. Vonage API Account: Sign up for a free Vonage API account. You'll receive free credit to start building.
  2. Node.js: Install Node.js version 18 or higher from nodejs.org.
  3. ngrok: Install ngrok and create a free account at ngrok.com.
  4. WhatsApp Account: Personal WhatsApp account on your smartphone for testing the WhatsApp Sandbox integration.
  5. Vonage Phone Number: Purchase a virtual phone number from your Vonage dashboard to send and receive SMS. Navigate to Numbers > Buy numbers.

System architecture

Your application interacts with the Vonage platform in two ways:

  1. Sending Messages: Your Node.js app uses the Vonage SDK to call the Vonage Messages API, specifying the channel (SMS or WhatsApp), recipient, sender ID (your Vonage number or WhatsApp Sandbox number), and message content. Vonage delivers the message via the respective network.
  2. Receiving Messages: When a user sends a message to your Vonage number (SMS) or the WhatsApp Sandbox number, Vonage sends an HTTP POST request (webhook) containing the message details to a preconfigured URL endpoint hosted by your Node.js application. During development, ngrok forwards these requests from the public internet to your local machine.
mermaid
graph TD
    subgraph Your Application
        NodeApp[Node.js/Express App]
        EnvFile[.env File] --> NodeApp
    end

    subgraph Vonage Platform
        MessagesAPI[Messages API]
        VonageNumber[Vonage SMS Number]
        WhatsAppSandbox[WhatsApp Sandbox]
        VonageApp[Vonage Application Config]
        Webhooks[Webhook Service]
    end

    subgraph External Networks
        SMSNetwork[SMS Network]
        WhatsAppNetwork[WhatsApp Network]
    end

    UserSMS[User (SMS)]
    UserWhatsApp[User (WhatsApp)]

    NodeApp -- Send Request (SDK) --> MessagesAPI
    MessagesAPI -- Deliver SMS --> SMSNetwork --> UserSMS
    MessagesAPI -- Deliver WhatsApp --> WhatsAppNetwork --> UserWhatsApp

    UserSMS -- Sends SMS --> SMSNetwork -- Inbound SMS --> VonageNumber
    UserWhatsApp -- Sends WhatsApp --> WhatsAppNetwork -- Inbound WhatsApp --> WhatsAppSandbox

    VonageNumber -- Associated with --> VonageApp
    WhatsAppSandbox -- Triggers Webhook --> Webhooks
    VonageApp -- Webhook URLs --> Webhooks

    Webhooks -- POST Request --> Ngrok[ngrok Tunnel] --> NodeApp

    style UserSMS fill:#f9f,stroke:#333,stroke-width:2px
    style UserWhatsApp fill:#9cf,stroke:#333,stroke-width:2px

Note: If this Mermaid diagram doesn't render in your Markdown processor, generate a static image as a fallback and include appropriate alt text.

1. Setting up the project

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

  1. Create Project Directory: Open your terminal 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 a new Node.js project using npm. The -y flag accepts default settings.

    bash
    npm init -y

    This creates a package.json file.

  3. Install Dependencies: Install Express for the web server, the Vonage SDKs for API interaction, and dotenv for managing environment variables.

    bash
    npm install express @vonage/server-sdk @vonage/messages @vonage/jwt dotenv
  4. Create Project Structure: Create a simple structure for your source code.

    bash
    mkdir src
    touch src/server.js
    touch .env
    touch .gitignore
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing them to version control. Never commit sensitive credentials.

    Code
    node_modules/
    .env
  6. Set Up Environment Variables (.env): Open the .env file and add the following variables. You'll fill these in during the Vonage setup section.

    dotenv
    # .env
    
    # Vonage API Credentials (found on Vonage Dashboard homepage)
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Vonage Application Details (generated when creating a Vonage Application)
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
    VONAGE_PRIVATE_KEY=./private.key # Path to your downloaded private key file (for local dev)
    
    # Vonage Numbers (your purchased Vonage number and the WhatsApp Sandbox number)
    VONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_SMS_NUMBER
    # Verify the Sandbox number from your Vonage Dashboard (Messages API Sandbox page)
    VONAGE_WHATSAPP_SANDBOX_NUMBER=14157386102
    
    # Webhook Security (found in Vonage Dashboard > Settings)
    VONAGE_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET
    
    # Server Configuration
    PORT=8000 # Port for the Express server
    • VONAGE_API_KEY, VONAGE_API_SECRET: Your main account credentials.
    • VONAGE_APPLICATION_ID: Identifies your specific Vonage application configuration.
    • VONAGE_PRIVATE_KEY: Path to the private key file used for authenticating requests related to your Vonage application (required for Messages API). For deployment, handle this differently (see Section 8).
    • VONAGE_SMS_FROM_NUMBER: Your purchased Vonage number in E.164 format (e.g., 12015550123).
    • VONAGE_WHATSAPP_SANDBOX_NUMBER: The number provided by Vonage for the WhatsApp Sandbox. Verify this on your dashboard.
    • VONAGE_SIGNATURE_SECRET: Used to verify that incoming webhooks originated from Vonage.
    • PORT: The local port your Express server listens on.

2. Vonage account and application setup

Configure your Vonage account and create a Vonage Application to handle messaging.

  1. Log in to Vonage Dashboard: Access your Vonage API Dashboard.
  2. Find API Key and Secret: On the dashboard homepage, locate your API key and API secret. Copy these values into the corresponding VONAGE_API_KEY and VONAGE_API_SECRET variables in your .env file.
  3. Create a Vonage Application:
    • Navigate to Applications > Create a new application.
    • Give your application a name (e.g., "Node Messaging App").
    • Click Generate public and private key. Important: A private.key file will download. Save this file in your project's root directory (same level as your .env file). Ensure the VONAGE_PRIVATE_KEY path in your .env file points to this file (e.g., ./private.key).
    • Enable Messages under Capabilities. Configure the webhook URLs later after starting ngrok. Leave the Inbound URL and Status URL fields blank for now.
    • Click Generate new application.
    • On the next screen, copy the Application ID and paste it into the VONAGE_APPLICATION_ID variable in your .env file.
  4. Link Your Vonage SMS Number:
    • Go back to your Application's settings page.
    • Scroll down to Link virtual numbers.
    • Find the Vonage phone number you purchased earlier and click Link. This associates incoming SMS messages on this number with your application's webhooks.
    • Copy this number in E.164 format (e.g., 12015550123) into the VONAGE_SMS_FROM_NUMBER variable in your .env file.
  5. Find Signature Secret:
    • Navigate to Settings in the main dashboard menu.
    • Under API settings, find your Signature secret. Copy this value and paste it into the VONAGE_SIGNATURE_SECRET variable in your .env file.
    • Important: Ensure Default SMS Setting is set to Use the Messages API. Save changes if needed.
  6. Set Up WhatsApp Sandbox:
    • Navigate to Messages API Sandbox in the sidebar (under Build & Manage).
    • You'll see QR codes and instructions to link your personal WhatsApp number to the sandbox. Scan the QR code with your phone's WhatsApp app or send the specified message to the Vonage Sandbox number (+14157386102).
    • Follow the prompts in WhatsApp to allowlist your number for testing.
    • Verify the VONAGE_WHATSAPP_SANDBOX_NUMBER in your .env file matches the number shown on the Sandbox page.
    • Add webhook URLs to the Sandbox configuration later.

3. Setting up the Express server

Now let's write the basic Express server code in 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 fs = require('fs'); // Needed for reading private key file locally

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

// ---- Environment Variables Check ----
const requiredEnv = [
  'VONAGE_API_KEY', 'VONAGE_API_SECRET', 'VONAGE_APPLICATION_ID',
  'VONAGE_PRIVATE_KEY', 'VONAGE_SMS_FROM_NUMBER',
  'VONAGE_WHATSAPP_SANDBOX_NUMBER', 'VONAGE_SIGNATURE_SECRET', 'PORT'
];
const missingEnv = requiredEnv.filter(key => !process.env[key]);
if (missingEnv.length > 0) {
  console.error(`Error: Missing required environment variables: ${missingEnv.join(', ')}`);
  console.error('Please check your .env file or environment configuration.');
  process.exit(1); // Exit if critical variables are missing
}

const PORT = process.env.PORT || 8000;
const VONAGE_SIGNATURE_SECRET = process.env.VONAGE_SIGNATURE_SECRET;

// ---- Initialize Vonage Client ----
// Determine private key source (file path for local dev, env var for production)
let privateKeyValue;
if (process.env.VONAGE_PRIVATE_KEY_CONTENT) {
    // Option 1: Read directly from an env var containing the key content
    privateKeyValue = process.env.VONAGE_PRIVATE_KEY_CONTENT.replace(/\\n/g, '\n'); // Handle potential escaped newlines
} else if (fs.existsSync(process.env.VONAGE_PRIVATE_KEY)) {
    // Option 2: Read from file path specified in .env (for local dev)
    privateKeyValue = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY);
} else {
    console.error('Error: VONAGE_PRIVATE_KEY path is invalid or VONAGE_PRIVATE_KEY_CONTENT env var is not set.');
    process.exit(1);
}

const vonage = new Vonage({
  apiKey: process.env.VONAGE_API_KEY, // Optional for this setup, but good practice
  apiSecret: process.env.VONAGE_API_SECRET, // Optional for this setup
  applicationId: process.env.VONAGE_APPLICATION_ID,
  privateKey: privateKeyValue // Use the loaded key content
}, {
  // Set apiHost for WhatsApp Sandbox testing
  // Remove or change this for production WhatsApp
  apiHost: 'https://messages-sandbox.nexmo.com'
});

console.log('Vonage client initialized.');

// ---- Webhook Security Middleware ----
const verifyVonageSignature = (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      console.warn('Webhook Error: Missing or invalid Authorization header.');
      return res.status(401).send('Unauthorized: Missing or invalid token');
    }
    const token = authHeader.split(' ')[1];

    // It's recommended to check `req.body` directly if possible,
    // but Express middleware might have already parsed it.
    // The SDK's verifySignature expects the raw body or the parsed object.
    if (verifySignature(token, VONAGE_SIGNATURE_SECRET, req.body)) {
      console.log('Webhook Signature Verified Successfully.');
      next(); // Signature is valid, proceed to the route handler
    } else {
      console.warn('Webhook Error: Invalid signature.');
      res.status(401).send('Unauthorized: Invalid signature');
    }
  } catch (error) {
    console.error('Webhook Signature Verification Error:', error);
    res.status(500).send('Internal Server Error during signature verification');
  }
};

// ---- Define Routes (We will add these later) ----
// Placeholder for sending messages API endpoint
app.post('/api/send-message', (req, res) => {
  res.status(501).send({ message: 'Send message endpoint not implemented yet.' });
});

// Placeholder for inbound message webhook
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
  console.log('Received Inbound Webhook (Stub)');
  res.status(200).send('OK'); // Always respond with 200 OK
});

// Placeholder for message status webhook
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
  console.log('Received Status Webhook (Stub)');
  res.status(200).send('OK'); // Always respond with 200 OK
});

// ---- Basic Error Handler ----
app.use((err, req, res, next) => {
  console.error("Unhandled Error:", err.stack || err);
  res.status(500).send('Something broke!');
});

// ---- Start Server ----
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
  console.log('Ensure ngrok is running and webhooks are configured correctly.');
});

Explanation:

  1. Load Env Vars: require('dotenv').config() loads variables from .env.
  2. Initialize Express: Sets up the Express application and middleware for parsing request bodies.
  3. Env Var Check: Verifies that all necessary environment variables are present.
  4. Initialize Vonage Client: Creates a Vonage instance. It now attempts to read the private key content either directly from an environment variable VONAGE_PRIVATE_KEY_CONTENT (for deployment) or from the file path specified in VONAGE_PRIVATE_KEY (for local development). The apiHost is set specifically for the WhatsApp Sandbox. Remove or change this for production WhatsApp.
  5. Webhook Security Middleware (verifyVonageSignature): Intercepts webhook requests, extracts the JWT token, verifies it using verifySignature, and passes control or sends an error response. Crucially, webhooks must be secured.
  6. Route Placeholders: Defines basic routes, applying verifyVonageSignature only to webhook routes.
  7. Basic Error Handler: A simple catch-all middleware.
  8. Start Server: Starts the Express server.

4. Sending messages

Let's implement the functions to send SMS and WhatsApp messages.

  1. Add Sending Functions: Add these functions inside src/server.js before the route definitions.
javascript
// src/server.js
// ... (Keep existing code above)

// ---- Message Sending Functions ----

/**
 * Sends an SMS message using the Vonage Messages API.
 * @param {string} toNumber - The recipient's phone number (E.164 format).
 * @param {string} text - The message content.
 * @returns {Promise<string>} - The message UUID on success.
 * @throws {Error} - If sending fails.
 */
const sendSmsMessage = async (toNumber, text) => {
  const fromNumber = process.env.VONAGE_SMS_FROM_NUMBER;
  console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`);
  try {
    const response = await vonage.messages.send({
      message_type: "text",
      text: text,
      to: toNumber,
      from: fromNumber,
      channel: "sms"
    });
    // Assuming SDK maps message_uuid to messageUuid based on code comments/context
    console.log(`SMS sent successfully to ${toNumber}. Message UUID: ${response.messageUuid}`);
    return response.messageUuid;
  } catch (error) {
    console.error(`Error sending SMS to ${toNumber}:`, error.response ? error.response.data : error.message);
    // Rethrow or handle specific errors (e.g., invalid number, insufficient funds)
    throw new Error(`Failed to send SMS: ${error.message}`);
  }
};

/**
 * Sends a WhatsApp message using the Vonage Messages API (Sandbox).
 * @param {string} toNumber - The recipient's WhatsApp number (E.164 format).
 * @param {string} text - The message content.
 * @returns {Promise<string>} - The message UUID on success.
 * @throws {Error} - If sending fails.
 */
const sendWhatsAppMessage = async (toNumber, text) => {
  // For Sandbox, 'from' is the Sandbox number. For production, it's your provisioned WhatsApp number.
  const fromNumber = process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER;
  console.log(`Attempting to send WhatsApp message from ${fromNumber} to ${toNumber}`);
  try {
    // Use the WhatsAppText message constructor for WhatsApp
    const response = await vonage.messages.send(
      new WhatsAppText({
        text: text,
        to: toNumber,
        from: fromNumber, // Using Sandbox number here
      })
    );
    // Assuming SDK maps message_uuid to messageUuid based on code comments/context
    console.log(`WhatsApp message sent successfully to ${toNumber}. Message UUID: ${response.messageUuid}`);
    return response.messageUuid;
  } catch (error) {
    console.error(`Error sending WhatsApp message to ${toNumber}:`, error.response ? error.response.data : error.message);
    // Handle specific WhatsApp errors (e.g., user not allowlisted in sandbox, template required for production)
    throw new Error(`Failed to send WhatsApp message: ${error.message}`);
  }
};

// ---- Webhook Security Middleware (Keep existing code) ----
// const verifyVonageSignature = ...

// ---- Define Routes ----
// Update the /api/send-message endpoint
app.post('/api/send-message', async (req, res) => {
  const { to, type, text } = req.body;

  // Basic Input Validation
  if (!to || !type || !text) {
    return res.status(400).send({ error: 'Missing required fields: to, type, text' });
  }
  if (type !== 'sms' && type !== 'whatsapp') {
    return res.status(400).send({ error: 'Invalid type. Must be "sms" or "whatsapp"' });
  }
  // Add more robust phone number validation if needed (e.g., using a library)

  try {
    let messageUuid;
    if (type === 'sms') {
      messageUuid = await sendSmsMessage(to, text);
    } else { // type === 'whatsapp'
      // Ensure the target number is allowlisted in the Sandbox for testing
      console.warn(`Sending WhatsApp to ${to}. Ensure this number is allowlisted in the Sandbox.`);
      messageUuid = await sendWhatsAppMessage(to, text);
    }
    res.status(200).send({ message: `${type.toUpperCase()} sent successfully`, messageUuid });
  } catch (error) {
    console.error(`Failed to send message via API endpoint: ${error.message}`);
    // Provide a more user-friendly error message
    res.status(500).send({ error: `Failed to send ${type.toUpperCase()} message.`, details: error.message });
  }
});

// ---- Webhook Routes (Keep existing code) ----
// app.post('/webhooks/inbound', verifyVonageSignature, ...
// app.post('/webhooks/status', verifyVonageSignature, ...

// ---- Basic Error Handler (Keep existing code) ----
// app.use((err, req, res, next) => ...

// ---- Start Server (Keep existing code) ----
// app.listen(PORT, () => ...

Explanation:

  • sendSmsMessage: Sends SMS using vonage.messages.send with channel: "sms".
  • sendWhatsAppMessage: Sends WhatsApp using the WhatsAppText constructor and the Sandbox number.
  • API Endpoint (/api/send-message): Handles POST requests to send messages, validates input, and calls the appropriate sending function.

5. Receiving messages (webhooks)

To receive messages, we need to expose our local server to the internet using ngrok and configure Vonage to send webhooks to that public URL.

  1. Start ngrok: Open a new terminal window and start ngrok, pointing it to the port your Express server is listening on (e.g., 8000).

    bash
    ngrok http 8000

    Copy the https:// Forwarding URL (e.g., https://<unique-subdomain>.ngrok-free.app). This is your public webhook base URL.

  2. Configure Vonage Webhooks:

    • Application Webhooks (for SMS):
      • Go back to your Vonage Application settings in the dashboard.
      • Under Messages capabilities, paste your ngrok Forwarding URL into the Inbound URL field, appending /webhooks/inbound.
      • Paste your ngrok Forwarding URL into the Status URL field, appending /webhooks/status.
      • Click Save changes.
    • WhatsApp Sandbox Webhooks:
      • Navigate to the Messages API Sandbox page in the dashboard.
      • Click on the Webhooks tab.
      • Paste your ngrok URL + /webhooks/inbound into the Inbound URL field.
      • Paste your ngrok URL + /webhooks/status into the Status URL field.
      • Click Save webhooks.
  3. Implement Webhook Handlers: Update the placeholder webhook routes in src/server.js to actually process the incoming data.

javascript
// src/server.js
// ... (Keep existing code for initialization, sending functions, middleware)

// ---- Define Routes ----
// app.post('/api/send-message', ... (Keep existing code)

// ---- Webhook Routes ----

// Inbound Message Webhook
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
  console.log('--- INBOUND WEBHOOK RECEIVED ---');
  console.log('Timestamp:', new Date().toISOString());
  // Log the full payload as structured JSON
  console.log('Request Body:', JSON.stringify(req.body, null, 2));

  const { channel, message_type, from, to, text, message_uuid } = req.body;

  // Process based on channel
  if (channel === 'sms') {
    console.log(`Received SMS from ${from} to ${to}`);
    if (message_type === 'text' && text) {
      console.log(`Message Text: "${text}"`);
      // --- Add your SMS processing logic here ---
      // Example: Look up user, trigger reply, store message
      // await processIncomingSms(from, text);
    }
  } else if (channel === 'whatsapp') {
    console.log(`Received WhatsApp message from ${from} to ${to}`);
    if (message_type === 'text' && text) {
      console.log(`Message Text: "${text}"`);
      // --- Add your WhatsApp processing logic here ---
      // Example: Handle commands, interact with chatbot logic
      // await processIncomingWhatsApp(from, text);
    }
    // Handle other WhatsApp message types (image, audio, location, etc.) if needed
    // else { console.log(`Received WhatsApp message of type: ${message_type}`); }

  } else {
    console.log(`Received message on unhandled channel: ${channel}`);
  }

  // CRITICAL: Always respond with 200 OK quickly to acknowledge receipt.
  res.status(200).send('OK');
});

// Message Status Webhook
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
  console.log('--- STATUS WEBHOOK RECEIVED ---');
  console.log('Timestamp:', new Date().toISOString());
  // Log the full payload as structured JSON
  console.log('Request Body:', JSON.stringify(req.body, null, 2));

  const { message_uuid, status, timestamp, error, usage } = req.body;

  console.log(`Status update for Message UUID: ${message_uuid}`);
  console.log(`Status: ${status} at ${timestamp}`);

  if (error) {
    console.error(`Error Code: ${error.code}, Reason: ${error.reason}`);
    // --- Add error handling logic here ---
    // Example: Log to error tracking service, notify admin, attempt retry if applicable
    // await handleFailedMessage(message_uuid, error);
  }

  if (usage) {
    console.log(`Message Cost: ${usage.price} ${usage.currency}`);
    // --- Add usage tracking logic here ---
  }

  // Potential statuses: submitted, delivered, rejected, undeliverable, read (WhatsApp)
  switch (status) {
      case 'delivered':
          console.log('Message successfully delivered.');
          break;
      case 'read':
          console.log('WhatsApp message read by recipient.');
          break;
      case 'rejected':
      case 'undeliverable':
          console.warn(`Message failed with status: ${status}`);
          // Consider adding specific handling for failed messages
          break;
      default:
          console.log(`Received status: ${status}`);
  }

  // CRITICAL: Always respond with 200 OK quickly.
  res.status(200).send('OK');
});

// ---- Basic Error Handler (Keep existing code) ----
// app.use((err, req, res, next) => ...

// ---- Start Server (Keep existing code) ----
// app.listen(PORT, () => ...

Explanation:

  • Inbound Webhook (/webhooks/inbound): Logs the full incoming payload using JSON.stringify for better structure. Differentiates processing based on channel. Responds with 200 OK promptly.
  • Status Webhook (/webhooks/status): Logs the status update payload using JSON.stringify. Provides details on message delivery, errors, and usage. Responds with 200 OK promptly.

6. Error handling and logging refinement

While we have basic try...catch and logging, let's refine it slightly. For production, consider dedicated logging libraries (pino, winston) and error tracking services (Sentry, Rollbar).

  • Structured Logging: Using JSON.stringify in the webhook handlers (as implemented above) is a step towards structured logging, making logs easier to parse. You can enhance this further by adopting a standard log format across your application.
    javascript
    // Example structured log (more detailed)
    console.log(JSON.stringify({
      level: 'info',
      timestamp: new Date().toISOString(),
      message: 'Received inbound SMS',
      details: { from: from, to: to, messageId: message_uuid }
    }));
    
    // Example structured error log
    console.error(JSON.stringify({
        level: 'error',
        timestamp: new Date().toISOString(),
        message: 'Failed to send SMS',
        error: { message: error.message, stack: error.stack }, // Include stack trace
        context: { recipient: toNumber, function: 'sendSmsMessage' }
    }));
  • Specific Error Handling: Catch specific Vonage API errors if possible by inspecting error.response.data or error.message for details provided by Vonage.
  • Webhook Retries: Remember Vonage retries webhooks if they don't receive a 200 OK. Your webhook handler should be idempotent (safe to run multiple times with the same input) or store processed message_uuids to prevent duplicate actions.
  • Retry Mechanisms (Sending): For transient network errors when sending messages, consider implementing a simple retry strategy with exponential backoff (e.g., using libraries like async-retry).

7. Verification and testing

Now, let's test the complete flow.

  1. Ensure Services are Running:

    • Your Node.js server (node src/server.js in one terminal).
    • ngrok (ngrok http <PORT> in another terminal).
  2. Test Sending SMS:

    • Use curl or Postman. Replace <YOUR_PERSONAL_PHONE_NUMBER> (E.164 format).
    bash
    curl -X POST http://localhost:8000/api/send-message \
         -H "Content-Type: application/json" \
         -d '{
               "to": "<YOUR_PERSONAL_PHONE_NUMBER>",
               "type": "sms",
               "text": "Hello from Vonage Node App! (SMS)"
             }'
    • Check: SMS received, server logs success, status webhooks arrive.
  3. Test Sending WhatsApp:

    • Ensure your number is allowlisted in the Sandbox. Replace <YOUR_PERSONAL_WHATSAPP_NUMBER> (E.164 format).
    bash
    curl -X POST http://localhost:8000/api/send-message \
         -H "Content-Type: application/json" \
         -d '{
               "to": "<YOUR_PERSONAL_WHATSAPP_NUMBER>",
               "type": "whatsapp",
               "text": "Hello from Vonage Node App! (WhatsApp Sandbox)"
             }'
    • Check: WhatsApp message received, server logs success, status webhooks arrive.
  4. Test Receiving SMS:

    • From your personal phone, send an SMS to your VONAGE_SMS_FROM_NUMBER.
    • Check: ngrok shows POST /webhooks/inbound, server logs signature verification, inbound webhook details (channel: sms), and message text.
  5. Test Receiving WhatsApp:

    • From your personal WhatsApp, send a message to the VONAGE_WHATSAPP_SANDBOX_NUMBER (+14157386102).
    • Check: ngrok shows POST /webhooks/inbound, server logs signature verification, inbound webhook details (channel: whatsapp), and message text.

8. Deployment and CI/CD considerations

Deploying this application requires replacing ngrok with a permanent public URL and managing environment variables securely.

  1. Choose a Hosting Platform: Options include Heroku, Vercel, AWS, Google Cloud, Azure, etc.
  2. Environment Variables: Configure your environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, etc.) directly on the hosting platform. Do not commit your .env file.
    • Private Key Handling: Instead of using a file path for VONAGE_PRIVATE_KEY, set the content of your private.key file as an environment variable (e.g., VONAGE_PRIVATE_KEY_CONTENT). Many platforms support multi-line environment variables. Ensure your src/server.js reads this variable, as shown in the updated initialization code in Section 3. You might need to handle newline characters (\n) correctly (e.g., replace literal \n with actual newlines if your platform escapes them).
    • Example Code Modification (already done in Section 3): The Vonage client initialization in src/server.js now checks for process.env.VONAGE_PRIVATE_KEY_CONTENT first before falling back to the file path.
  3. Public URL: Your hosting platform will provide a public URL (e.g., https://your-app-name.herokuapp.com).
  4. Update Vonage Webhooks: Replace the ngrok URLs in your Vonage Application and Messages API Sandbox settings with your permanent public URL (e.g., https://your-app-name.herokuapp.com/webhooks/inbound and /status).
  5. Procfile (for Heroku/similar): You might need a Procfile:
    Procfile
    web: node src/server.js
  6. CI/CD Pipeline: Set up a pipeline (GitHub Actions, GitLab CI, etc.) for automated testing, building, and deployment, managing environment variables securely.

9. Troubleshooting and caveats

  • Invalid Credentials: Double-check API keys, secret, Application ID, and private key content/path.
  • Webhook Failures (401 Unauthorized): Incorrect VONAGE_SIGNATURE_SECRET or issues with the verifySignature implementation.
  • Webhook Failures (No Response / Timeouts): Server not responding 200 OK quickly, or the server crashed. Check server logs. Vonage expects a response within a few seconds.
  • WhatsApp Sandbox Limitations: Only works with allowlisted numbers. Message templates are required for production WhatsApp outside the 24-hour customer care window. The apiHost needs to be changed for production WhatsApp.
  • Rate Limits: Be aware of Vonage API rate limits. Implement appropriate error handling and backoff strategies.
  • Number Formatting: Ensure all phone numbers are in E.164 format (e.g., +12015550123).
  • SMS Sender ID: SMS sender ID behavior varies by country. Your Vonage number might be overwritten in some regions.
  • Idempotency: Ensure webhook handlers are idempotent to handle potential retries from Vonage safely.