code examples

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

Building SMS Interactions with Node.js, Express, and Vonage

A step-by-step guide to creating a Node.js application using Express and the Vonage Messages API for sending SMS, receiving messages, and handling delivery receipts via webhooks.

This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to send and receive SMS messages via the Vonage Messages API. We will cover project setup, sending messages, handling incoming messages and delivery receipts via webhooks, configuration, error handling, security considerations, and deployment.

By the end of this tutorial, you will have a functional application capable of programmatic SMS communication, forming the backbone for SMS marketing campaigns, notifications, or interactive services. We will use Node.js for its asynchronous capabilities, Express for its robust web framework features, and the Vonage Messages API for its versatile multi-channel communication support, focusing specifically on SMS.

Project Overview and Goals

What We'll Build:

A Node.js application that can:

  1. Send SMS messages programmatically using the Vonage Messages API.
  2. Receive incoming SMS messages sent to a Vonage virtual number via an Express webhook.
  3. Receive message delivery status updates (Delivery Receipts - DLRs) via an Express webhook.

Problem Solved:

This application provides the core infrastructure needed to integrate SMS capabilities into larger systems. It enables businesses to automate sending messages for marketing, alerts, or two-factor authentication, and to process replies or status updates programmatically.

Technologies Used:

  • Node.js: A JavaScript runtime environment ideal for building scalable, asynchronous network applications.
  • Express: A minimal and flexible Node.js web application framework providing robust features for web and mobile applications.
  • Vonage Messages API: A powerful API enabling communication across multiple channels (SMS, MMS, WhatsApp, etc.). We will focus on its SMS capabilities.
  • @vonage/server-sdk: The official Vonage Node.js SDK for interacting with the Vonage APIs.
  • ngrok: A tool to expose local development servers to the internet, essential for testing webhooks.
  • dotenv: A module to load environment variables from a .env file into process.env.

System Architecture:

mermaid
graph LR
    subgraph Your Application
        direction LR
        A[Node.js/Express App] -->|Sends SMS via SDK| B(Vonage SDK);
        C(Webhook Endpoint: /webhooks/inbound) <-. Receives Inbound SMS .- D(Vonage Platform);
        E(Webhook Endpoint: /webhooks/status) <-. Receives DLRs .- D;
    end

    subgraph External
        direction LR
        B -->|API Call (HTTPS)| D;
        D -->|Sends SMS| F([End User Phone]);
        F -->|Sends SMS| D;
    end

    style Your Application fill:#f9f,stroke:#333,stroke-width:2px
    style External fill:#ccf,stroke:#333,stroke-width:2px

Prerequisites:

  • A Vonage API account (Sign up free).
  • Node.js and npm (or yarn) installed (Download Node.js).
  • A Vonage virtual phone number capable of sending/receiving SMS.
  • ngrok installed and authenticated (Download ngrok).
  • Basic understanding of JavaScript, Node.js, and REST APIs.
  • (Optional) Vonage CLI installed globally: npm install -g @vonage/cli.

1. Setting up the Project

This section details setting up the Node.js project structure, installing dependencies, and configuring the environment.

1. Create Project Directory:

Open your terminal and create a new directory for your project, then navigate into it.

bash
mkdir vonage-sms-app
cd vonage-sms-app

2. Initialize Node.js Project:

Initialize the project using npm (or yarn). This creates a package.json file.

bash
npm init -y

3. Install Dependencies:

Install the necessary libraries: Express for the web server, the Vonage Server SDK to interact with the API, and dotenv for managing environment variables.

bash
npm install express @vonage/server-sdk dotenv

4. Create Project Files:

Create the main files for our application logic and environment configuration.

bash
touch index.js server.js .env .gitignore
  • index.js: Will contain the code for sending SMS messages.
  • server.js: Will contain the Express server code for receiving SMS messages and status updates via webhooks.
  • .env: Will store sensitive credentials and configuration variables.
  • .gitignore: Will specify files and directories that Git should ignore.

5. Configure .gitignore:

Open .gitignore and add the following lines to prevent committing sensitive information and unnecessary files:

text
node_modules/
.env
private.key
*.log

6. Set up Environment Variables (.env):

Open the .env file and add the following placeholders. We will populate these values later.

dotenv
# Vonage API Credentials (Find in Vonage Dashboard -> API Settings)
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET

# Vonage Application Credentials (Generated when creating a Vonage Application)
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key

# Vonage Number (Purchase in Vonage Dashboard -> Numbers)
VONAGE_NUMBER=YOUR_VONAGE_NUMBER

# Recipient Number (For testing sending SMS)
RECIPIENT_NUMBER=RECIPIENT_PHONE_NUMBER_WITH_COUNTRY_CODE

# Server Port
PORT=3000

Explanation of Configuration:

  • Storing credentials (API Key, API Secret, Application ID, Private Key Path) in environment variables (.env file locally, system environment variables in production) is crucial for security. It avoids hardcoding sensitive data directly into the source code.
  • The dotenv library enables loading these variables into process.env automatically during development.
  • VONAGE_NUMBER is the virtual number purchased from Vonage that will send and receive messages.
  • RECIPIENT_NUMBER is the target phone number for sending test messages (use your own mobile number). Ensure it includes the country code (e.g., 14155552671).
  • PORT defines the port our Express server will listen on.

2. Implementing Core Functionality - Sending SMS

Let's implement the logic to send an SMS message using the Vonage Node.js SDK.

1. Edit index.js:

Open index.js and add the following code:

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

const { Vonage } = require('@vonage/server-sdk');
const { MESSAGES_SANDBOX_URL } = require('@vonage/server-sdk'); // Use sandbox for testing if needed

// --- Configuration ---
const vonageApiKey = process.env.VONAGE_API_KEY;
const vonageApiSecret = process.env.VONAGE_API_SECRET;
const vonageAppId = process.env.VONAGE_APPLICATION_ID;
const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH;
const vonageNumber = process.env.VONAGE_NUMBER;
const recipientNumber = process.env.RECIPIENT_NUMBER;

// --- Input Validation (Basic Example) ---
if (!vonageAppId || !privateKeyPath || !vonageNumber || !recipientNumber) {
    console.error('Error: Missing required environment variables for sending SMS.');
    console.error('Please check your .env file and ensure VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_NUMBER, and RECIPIENT_NUMBER are set.');
    process.exit(1); // Exit if configuration is missing
}

// --- Initialize Vonage Client ---
// The Messages API primarily uses Application ID and Private Key for authentication
const vonage = new Vonage({
    applicationId: vonageAppId,
    privateKey: privateKeyPath,
    // Optional: Use the sandbox endpoint for testing without sending real SMS
    // apiHost: MESSAGES_SANDBOX_URL
}, { debug: false }); // Set debug: true for detailed SDK logging

// --- Define Send Function ---
async function sendSms(to, from, text) {
    console.log(`Attempting to send SMS from ${from} to ${to}: ""${text}""`);
    try {
        const resp = await vonage.messages.send({
            message_type: ""text"",
            text: text,
            to: to,
            from: from,
            channel: ""sms""
        });
        console.log('SMS submitted successfully!');
        console.log('Message UUID:', resp.message_uuid);
        return resp; // Return the response object
    } catch (err) {
        console.error('Error sending SMS:');
        // Log specific Vonage error details if available
        if (err.response && err.response.data) {
            console.error('Vonage Error:', JSON.stringify(err.response.data, null, 2));
        } else {
            console.error(err);
        }
        // Re-throw the error or handle it as needed
        throw err;
    }
}

// --- Execute Send Function ---
// Ensure this part only runs when the script is executed directly
if (require.main === module) {
    const messageText = `Hello from Vonage via Node.js! Sent on ${new Date().toLocaleTimeString()}`;

    sendSms(recipientNumber, vonageNumber, messageText)
        .then(() => console.log('sendSms function completed.'))
        .catch(() => console.error('sendSms function failed.'));
}

// Export the function if you want to use it as a module elsewhere
module.exports = { sendSms };

Explanation:

  1. require('dotenv').config();: Loads variables from the .env file. Crucial to do this first.
  2. SDK Initialization: We create a Vonage client instance. For the Messages API, authentication primarily relies on the applicationId and privateKey (obtained in the Vonage Application setup step later). API Key/Secret might be used for other Vonage APIs or account management tasks.
  3. vonage.messages.send({...}): This is the core function call.
    • message_type: ""text"": Specifies a standard text message.
    • text: The content of the SMS.
    • to: The recipient's phone number (from .env).
    • from: Your Vonage virtual number (from .env).
    • channel: ""sms"": Explicitly uses the SMS channel of the Messages API.
  4. Asynchronous Handling: The send method returns a Promise. We use async/await for cleaner handling and include a try...catch block for error management.
  5. Error Logging: The catch block logs errors, including specific Vonage API error details if available in err.response.data.
  6. Execution: The if (require.main === module) block ensures the sendSms function is called only when index.js is run directly (e.g., node index.js), not when imported as a module.
  7. Export: We export sendSms so it could potentially be reused in other parts of a larger application (e.g., triggered by an API call in server.js).
  8. Note: This guide separates sending logic (index.js) from receiving logic (server.js) for clarity. In a more complex application, you might integrate the sending functionality into API routes within the main server.js Express application.

Alternative Approaches:

  • Callbacks: The SDK methods also support traditional Node.js callbacks if you prefer that style over Promises/async/await.
  • SMS API vs Messages API: Vonage has an older SMS API. The Messages API is generally preferred for new development due to its multi-channel support and richer features, even if only using SMS initially. Ensure your Vonage account settings use the Messages API as default (covered later).

3. Building the API Layer - Receiving SMS and Status Updates

Now, let's set up the Express server to handle incoming webhooks from Vonage.

1. Edit server.js:

Open server.js and add the following code:

javascript
// server.js
require('dotenv').config(); // Load environment variables

const express = require('express');
const { json, urlencoded } = express; // Use built-in parsers

const app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000

// --- Middleware ---
// Parse JSON request bodies
app.use(json());
// Parse URL-encoded request bodies (needed for some webhook formats)
app.use(urlencoded({ extended: true }));

// --- Basic Logging Middleware ---
app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    // Log body for POST requests (be cautious with sensitive data in production logs)
    if (req.method === 'POST' && req.body) {
         console.log('Request Body:', JSON.stringify(req.body, null, 2));
    }
    next(); // Pass control to the next middleware/route handler
});

// --- Webhook Endpoints ---

// 1. Inbound SMS Webhook
// Vonage sends incoming SMS messages to this endpoint
app.post('/webhooks/inbound', (req, res) => {
    console.log('--- Inbound SMS Received ---');
    const { msisdn, to, text, messageId, 'message-timestamp': timestamp } = req.body;

    if (!msisdn || !to || !text) {
        console.error('Invalid inbound message data received:', req.body);
        // Respond with error, but still 200 OK for Vonage not to retry unnecessarily for format errors
        return res.status(200).send('Invalid data format');
    }

    console.log(`From: ${msisdn}`); // Sender's number
    console.log(`To: ${to}`);     // Your Vonage number
    console.log(`Text: ${text}`);
    console.log(`Message ID: ${messageId}`);
    console.log(`Timestamp: ${timestamp}`);

    // --- TODO: Add your business logic here ---
    // - Store the message in a database
    // - Check for keywords (e.g., STOP, HELP)
    // - Trigger replies or other actions

    // --- Acknowledge Receipt to Vonage ---
    // Vonage requires a 200 OK response to confirm receipt.
    // Failure to respond or non-200 status will cause Vonage to retry.
    res.status(200).end();
});

// 2. Message Status (Delivery Receipt - DLR) Webhook
// Vonage sends status updates about outbound messages to this endpoint
app.post('/webhooks/status', (req, res) => {
    console.log('--- Message Status Received (DLR) ---');
    const { message_uuid, status, timestamp, to, from, error } = req.body;

    console.log(`Message UUID: ${message_uuid}`);
    console.log(`Status: ${status}`); // e.g., delivered, failed, accepted, rejected
    console.log(`Timestamp: ${timestamp}`);
    console.log(`To: ${to}`);
    console.log(`From: ${from}`);

    if (error) {
        console.error(`Error Code: ${error.code}`);
        console.error(`Error Reason: ${error.reason}`);
    }

    // --- TODO: Add your business logic here ---
    // - Update message status in your database using message_uuid
    // - Analyze delivery rates
    // - Trigger alerts for failed messages

    // --- Acknowledge Receipt to Vonage ---
    res.status(200).end();
});

// --- Health Check Endpoint ---
app.get('/health', (req, res) => {
    res.status(200).send('OK');
});

// --- Start Server ---
app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
    console.log(`Webhook endpoints expected at:`);
    console.log(`  Inbound SMS: POST /webhooks/inbound`);
    console.log(`  Status (DLR): POST /webhooks/status`);
    console.log(`Make sure ngrok forwards to this port.`);
});

// Export app for potential testing
module.exports = app;

Explanation:

  1. Middleware:
    • express.json(): Parses incoming requests with JSON payloads (common for modern webhooks).
    • express.urlencoded(): Parses incoming requests with URL-encoded payloads (sometimes used by older systems or form submissions). extended: true allows for rich objects and arrays.
    • Logging Middleware: A simple custom middleware logs every incoming request's method and URL. It also logs the body of POST requests (useful for debugging webhooks). Caution: In production, be mindful of logging sensitive data from request bodies.
  2. /webhooks/inbound:
    • Handles POST requests to this path. Vonage sends data about incoming SMS messages here.
    • Extracts key information from req.body (sender msisdn, recipient to, text, etc.).
    • Includes a placeholder // TODO: section where you would add logic to process the message (database storage, keyword checks, auto-replies).
    • Crucially, sends a res.status(200).end() response. Vonage requires this acknowledgment. Without it, Vonage assumes the webhook failed and will retry sending the message, leading to duplicate processing.
  3. /webhooks/status:
    • Handles POST requests for Delivery Receipts (DLRs). Vonage sends updates on the status of messages you sent.
    • Extracts key information like the message_uuid (which matches the UUID returned when you sent the message), status (delivered, failed, accepted, etc.), and any error details.
    • Includes a // TODO: section for logic like updating your database records based on the delivery status.
    • Also sends res.status(200).end() to acknowledge receipt.
  4. /health: A simple GET endpoint often used by monitoring systems or load balancers to check if the application is running.
  5. app.listen: Starts the Express server on the configured port.

Testing Webhooks:

Since these endpoints need to be publicly accessible for Vonage to reach them, we'll use ngrok during development. Testing involves:

  1. Running the Express server (node server.js).
  2. Running ngrok to expose the local port (ngrok http 3000).
  3. Configuring the ngrok URL in the Vonage dashboard (next section).
  4. Sending an SMS to your Vonage number (tests /inbound).
  5. Sending an SMS from your application (node index.js) and observing the status update (tests /status).

You can use tools like Postman or curl to manually send simulated webhook data to your local server before setting up ngrok, helping test the endpoint logic in isolation.

Example curl command for /webhooks/inbound:

bash
curl -X POST http://localhost:3000/webhooks/inbound \
-H ""Content-Type: application/json"" \
-d '{
  ""msisdn"": ""14155551234"",
  ""to"": ""12125559876"",
  ""messageId"": ""abc-def-ghi-jkl"",
  ""text"": ""Hello from curl test!"",
  ""type"": ""text"",
  ""keyword"": ""HELLO"",
  ""message-timestamp"": ""2025-04-20T12:00:00Z""
}'

Example curl command for /webhooks/status:

bash
curl -X POST http://localhost:3000/webhooks/status \
-H ""Content-Type: application/json"" \
-d '{
  ""message_uuid"": ""xyz-789-lmo-456"",
  ""status"": ""delivered"",
  ""timestamp"": ""2025-04-20T12:05:00Z"",
  ""to"": ""14155551234"",
  ""from"": ""12125559876"",
  ""usage"": { ""currency"": ""USD"", ""price"": ""0.0075""},
  ""client_ref"": ""my-campaign-123""
}'
# Example for a failed message
# curl -X POST http://localhost:3000/webhooks/status \
# -H ""Content-Type: application/json"" \
# -d '{
#   ""message_uuid"": ""xyz-789-lmo-457"",
#   ""status"": ""failed"",
#   ""timestamp"": ""2025-04-20T12:06:00Z"",
#   ""to"": ""14155551111"",
#   ""from"": ""12125559876"",
#   ""error"": { ""code"": 4, ""reason"": ""Invalid Destination Address""}
# }'

4. Integrating with Vonage Service

This section covers configuring your Vonage account and application to work with the code we've written.

1. Obtain API Key and Secret:

  • Log in to your Vonage API Dashboard.
  • On the main dashboard page (""Getting started""), you'll find your API key and API secret.
  • Copy these values and paste them into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.

2. Set Default SMS API to ""Messages API"":

  • Navigate to Settings in the left-hand menu.
  • Scroll down to the API settings section.
  • Under Default SMS Setting, ensure Messages API is selected. If not, select it and click Save changes. This ensures consistency between the API used for sending and the format of webhooks received.

3. Purchase a Vonage Number:

  • Navigate to Numbers -> Buy numbers.
  • Search for numbers based on country, features (SMS, Voice), and type (Mobile, Landline, Toll-free).
  • Choose a number that supports SMS and purchase it.
  • Note the full number (including country code). Paste this into your .env file for VONAGE_NUMBER.

4. Create a Vonage Application:

This application links your code (via API credentials) and your Vonage number to the webhook URLs.

  • Navigate to Your applications -> Create a new application.
  • Give your application a descriptive Name (e.g., ""NodeJS SMS Campaign App"").
  • Click Generate public and private key. This will automatically download a file named private.key.
    • Security: Save this private.key file in the root directory of your Node.js project (e.g., alongside package.json). Ensure it's listed in your .gitignore file.
    • Update the VONAGE_PRIVATE_KEY_PATH in your .env file to ./private.key.
  • Note the Application ID displayed on the page. Copy this value and paste it into your .env file for VONAGE_APPLICATION_ID.
  • Enable the Messages capability by toggling the switch next to it. This reveals fields for Inbound URL and Status URL.
  • Configure Webhook URLs (Using ngrok):
    • Open a new terminal window.
    • Navigate to your project directory (cd vonage-sms-app).
    • Start your Express server: node server.js.
    • Open another new terminal window.
    • Start ngrok to expose port 3000 (or the port your server is running on):
      bash
      ngrok http 3000
    • ngrok will display forwarding URLs. Copy the HTTPS forwarding URL (e.g., https://random-string.ngrok-free.app).
    • Go back to the Vonage Application configuration page.
    • Paste the HTTPS ngrok URL into the Inbound URL field and append /webhooks/inbound. (e.g., https://random-string.ngrok-free.app/webhooks/inbound).
    • Paste the HTTPS ngrok URL into the Status URL field and append /webhooks/status. (e.g., https://random-string.ngrok-free.app/webhooks/status).
  • Scroll down to the Link virtual numbers section.
  • Find the Vonage number you purchased earlier and click the Link button next to it.
  • Finally, click Save changes at the bottom of the page.

Summary of .env values and where to find them:

VariablePurposeHow to Obtain
VONAGE_API_KEYAccount-level authentication (legacy/other APIs)Vonage Dashboard -> Getting started
VONAGE_API_SECRETAccount-level authentication (legacy/other APIs)Vonage Dashboard -> Getting started
VONAGE_APPLICATION_IDIdentifies your specific Vonage ApplicationVonage Dashboard -> Your applications -> (Select your app) -> Application ID
VONAGE_PRIVATE_KEY_PATHPath to the private key for application authSet to ./private.key (after downloading during app creation)
VONAGE_NUMBERThe Vonage virtual number to useVonage Dashboard -> Numbers -> Your numbers
RECIPIENT_NUMBERTest destination numberYour own mobile number (include country code)
PORTLocal server portSet to 3000 (or your preference, ensure ngrok matches)

5. Implementing Error Handling, Logging, and Retry Mechanisms

Robust applications require solid error handling and logging.

Error Handling Strategy:

  1. SDK Errors (index.js): Wrap vonage.messages.send calls in try...catch blocks. Inspect the caught err object. Vonage SDK errors often contain err.response.data with specific API error details (code, reason). Log these details. Decide whether to retry based on the error type (e.g., temporary network issues vs. invalid number).
  2. Webhook Input Validation (server.js): Check for the presence of expected fields in req.body for /inbound and /status webhooks. If essential data is missing, log an error but still return 200 OK to Vonage – the issue is with the data payload, not the webhook receipt itself. Retrying won't fix malformed data.
  3. Webhook Processing Errors (server.js): If an error occurs during your business logic (e.g., database connection fails while trying to save an incoming message), log the error comprehensively. You must still return 200 OK to Vonage. Implement your own retry mechanism or dead-letter queue for your internal processing if necessary. Vonage's retry is only for confirming receipt of the webhook.
  4. Unexpected Server Errors (server.js): Implement a global Express error handler to catch unhandled exceptions and prevent stack traces from leaking.

Logging:

  • Development: console.log and console.error are sufficient. Set debug: true in the Vonage SDK options (new Vonage({...}, { debug: true })) for verbose SDK logging.
  • Production: Use a dedicated logging library like Winston or Pino.
    • Structured Logging: Log in JSON format for easier parsing by log aggregation tools (e.g., ELK Stack, Datadog, Splunk).
    • Log Levels: Use appropriate levels (debug, info, warn, error, fatal). Configure the minimum log level based on the environment (e.g., info in production, debug in development).
    • Log Correlation: Include unique identifiers (like the message_uuid or a request ID) in related log entries to trace requests across services.

Example (Adding Winston to server.js):

bash
npm install winston
javascript
// server.js (Top section)
const winston = require('winston');

// --- Configure Winston Logger ---
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info', // Default to info level
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }), // Log stack traces
    winston.format.json() // Log in JSON format
  ),
  transports: [
    // In production, you might write to files:
    // new winston.transports.File({ filename: 'error.log', level: 'error' }),
    // new winston.transports.File({ filename: 'combined.log' }),
    // In development, log to the console
    new winston.transports.Console({
        format: winston.format.combine(
            winston.format.colorize(), // Add colors
            winston.format.simple() // Simple format for console
        )
    })
  ],
  exceptionHandlers: [ // Catch unhandled exceptions
      // new winston.transports.File({ filename: 'exceptions.log' })
      new winston.transports.Console({ // Also log exceptions to console
          format: winston.format.combine(
              winston.format.colorize(),
              winston.format.simple()
          )
      })
  ],
  rejectionHandlers: [ // Catch unhandled promise rejections
     // new winston.transports.File({ filename: 'rejections.log' })
      new winston.transports.Console({ // Also log rejections to console
          format: winston.format.combine(
              winston.format.colorize(),
              winston.format.simple()
          )
      })
  ]
});

// --- Replace console.log/error with logger ---
// Example in /webhooks/inbound:
app.post('/webhooks/inbound', (req, res) => {
    logger.info('--- Inbound SMS Received ---', { body: req.body }); // Include context
    const { msisdn, to, text, messageId, 'message-timestamp': timestamp } = req.body; // Ensure variables are defined here

    if (!msisdn || !to || !text) {
        logger.error('Invalid inbound message data received', { body: req.body });
        return res.status(200).send('Invalid data format');
    }
    logger.info('Processing inbound SMS', { from: msisdn, to: to, messageId: messageId });
    // ... rest of the logic
    res.status(200).end();
});

// Example in index.js sendSms function (assuming logger is exported or passed):
// (This requires refactoring index.js or sharing the logger instance)
// async function sendSms(to, from, text, logger) { // Pass logger instance
//     logger.info(`Attempting to send SMS`, { to, from }); // Removed text from log for brevity/privacy
//     try {
//         const vonage = new Vonage(...) // Ensure vonage is initialized
//         const resp = await vonage.messages.send({ /* ... params ... */ });
//         logger.info('SMS submitted successfully', { message_uuid: resp.message_uuid });
//         return resp;
//     } catch (err) {
//         logger.error('Error sending SMS', {
//             error: err.message,
//             stack: err.stack,
//             vonage_response: err.response?.data // Safely access nested property
//         });
//         throw err;
//     }
// }

Retry Mechanisms:

  • Webhook Delivery (Vonage -> Your App): Vonage handles retries automatically if your /webhooks/inbound or /webhooks/status endpoints do not return a 200 OK response within a reasonable timeframe (usually a few seconds). They use an exponential backoff strategy. This is why acknowledging receipt quickly with res.status(200).end() is critical, even if your internal processing fails later.
  • SMS Sending (Your App -> Vonage): If sending an SMS fails due to potentially temporary issues (e.g., network timeout, Vonage API temporary unavailability - 5xx errors), you might implement application-level retries.
    • Use libraries like async-retry or p-retry.
    • Implement exponential backoff to avoid overwhelming the API.
    • Only retry on specific error codes indicating transient failures. Do not retry on permanent errors like ""Invalid Credentials"" (401) or ""Invalid Number Format"" (often 400).

Example (Basic retry logic in index.js - conceptual):

javascript
// index.js - Conceptual Retry (using pseudo-code logic)
// Assumes logger is available (e.g., passed as argument or imported)
const MAX_RETRIES = 3;
const INITIAL_DELAY_MS = 1000;

async function sendSmsWithRetry(to, from, text, logger) { // Pass logger
    let retries = 0;
    while (retries < MAX_RETRIES) {
        try {
            logger.info(`Attempt ${retries + 1} to send SMS`, { to, from });
            // Assuming 'vonage' client is initialized and available in scope
            const resp = await vonage.messages.send({
                message_type: ""text"",
                text: text,
                to: to,
                from: from,
                channel: ""sms""
             });
            logger.info('SMS submitted successfully', { message_uuid: resp.message_uuid });
            return resp; // Success! Exit loop.
        } catch (err) {
            logger.error(`Attempt ${retries + 1} failed`, { error: err.message, vonage_response: err.response?.data });

            // Check if the error is retryable (e.g., 5xx server errors, network issues)
            // This logic needs refinement based on specific Vonage error codes
            const isRetryable = err.response?.status >= 500 || ['ETIMEDOUT', 'ECONNRESET', 'EPIPE'].includes(err.code);

            if (isRetryable && retries < MAX_RETRIES - 1) {
                retries++;
                const delay = INITIAL_DELAY_MS * Math.pow(2, retries - 1); // Exponential backoff
                logger.warn(`Retrying in ${delay}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay)); // Wait
            } else {
                logger.error('Non-retryable error or max retries reached. Giving up.');
                throw err; // Re-throw the error for upstream handling
            }
        }
    }
}

// Call the retry function instead of the basic one
// Make sure to pass the logger instance
// sendSmsWithRetry(recipientNumber, vonageNumber, messageText, logger) ...

6. Creating a Database Schema and Data Layer (Conceptual)

While this guide focuses on the core Vonage integration, a real-world application requires persistent storage.

Why a Database?

  • Store contact lists or campaign recipients.
  • Log sent and received messages for history and auditing.
  • Track the delivery status (message_uuid, status) of outbound messages.
  • Manage opt-out lists (essential for compliance).
  • Store campaign details and results.

Frequently Asked Questions

How to send SMS messages with Node.js?

Use the Vonage Messages API with the Node.js Server SDK. After setting up a Vonage application and obtaining necessary credentials, the `vonage.messages.send()` function handles sending, specifying the recipient, sender, message type, content, and channel as 'sms'.

What is the Vonage Messages API used for?

The Vonage Messages API is a multi-channel communication platform enabling messaging across various channels like SMS, MMS, WhatsApp, and more. This tutorial focuses on its SMS capabilities for sending and receiving text messages.

Why use Express.js with Vonage Messages API?

Express.js simplifies building robust webhooks to receive incoming SMS messages and delivery receipts (DLRs) from Vonage. Its middleware handles request parsing and routing, making webhook management efficient.

When should I use the Vonage Messages API sandbox?

Use the sandbox URL (`MESSAGES_SANDBOX_URL`) during development and testing to avoid sending real SMS messages and incurring charges. Switch to the live API for production deployments.

Can I receive SMS messages to my Node.js app?

Yes, create a webhook endpoint (e.g., `/webhooks/inbound`) in your Express app. Configure this URL in your Vonage application settings. Vonage will forward incoming messages to this endpoint.

How to handle inbound SMS messages in Express?

In your `/webhooks/inbound` route handler, parse the incoming request body (`req.body`) which contains the sender's number (`msisdn`), message content (`text`), and other metadata. Always respond with `res.status(200).end()` to acknowledge receipt.

What are delivery receipts (DLRs) with Vonage?

DLRs provide status updates on the messages you've sent via Vonage. Your `/webhooks/status` endpoint receives these updates, indicating whether a message was 'delivered', 'failed', or is in another state.

How to set up ngrok with Vonage webhooks?

After starting your local Express server, run `ngrok http <your_port>` (e.g., `ngrok http 3000`). Use the generated HTTPS ngrok URL as the base for your webhook URLs in the Vonage application settings, appending the specific paths like `/webhooks/inbound`.

What is the @vonage/server-sdk?

The official Vonage Server SDK for Node.js facilitates interaction with Vonage APIs. It handles authentication, simplifies API calls, and provides helper functions for common tasks.

Why does Vonage require a 200 OK response for webhooks?

A 200 OK response confirms to Vonage that your application has received the webhook. Without it, Vonage assumes a failure and will retry sending the webhook, potentially causing duplicate processing.

How to secure Vonage API credentials in Node.js?

Store sensitive credentials (API key, secret, application ID, private key path) in environment variables (`.env` file locally) and avoid hardcoding them directly in your code. Load these variables using the `dotenv` library.

What is the Vonage application ID used for?

The Vonage application ID uniquely identifies your application within the Vonage platform and is used, along with the private key, for authenticating API requests, especially for the Messages API.

When to use API Key/Secret vs Application ID/Private Key?

The Messages API primarily uses the Application ID and Private Key for authentication. API Key and Secret might be used for other Vonage APIs or account management operations not related to sending/receiving messages.

How to test Vonage webhook endpoints locally?

Use `ngrok` to expose your local server or tools like Postman or `curl` to simulate webhook requests to your local endpoints (e.g., `http://localhost:3000/webhooks/inbound`) with appropriate test data.