code examples

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

Implementing SMS Delivery Status Webhooks with Node.js

A guide on setting up a Node.js/Express application using the Vonage Messages API to receive and process SMS delivery status updates via webhooks.

Tracking the delivery status of your SMS messages is crucial for building reliable communication workflows. Knowing whether a message reached the recipient's handset – or why it failed – enables you to build smarter retry logic, provide accurate user feedback, and gain valuable insights into message deliverability.

This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to receive and process SMS delivery status updates from the Vonage Messages API via webhooks. We will cover project setup, Vonage configuration, implementing the webhook handler, sending test messages, handling errors, and preparing for production deployment.

Project Goal: To create a robust Node.js service that can:

  1. Send SMS messages using the Vonage Messages API.
  2. Receive real-time delivery status updates for those messages via a secure webhook endpoint.
  3. Log or store these status updates for analysis or further action.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express: A minimal and flexible Node.js web application framework used to create the webhook endpoint.
  • Vonage Messages API: A multi-channel API for sending and receiving messages, including SMS. We'll use it for sending SMS and receiving status updates.
  • @vonage/server-sdk: The official Vonage Node.js SDK for interacting with the API.
  • ngrok: A tool to expose local servers to the internet, essential for testing webhooks during development.
  • dotenv: A module to load environment variables from a .env file.
  • jsonwebtoken: (Optional, for Security) A library to verify JWT signatures on incoming webhooks.

Prerequisites:

  • A Vonage API account. Sign up here if you don't have one.
  • Node.js and npm (or yarn) installed locally.
  • A publicly accessible Vonage virtual phone number capable of sending SMS. You can purchase one from the Vonage API Dashboard.
  • ngrok installed and authenticated. Download it here.

System Architecture

The flow involves two main interactions with the Vonage platform:

  1. Sending SMS: Your Node.js application uses the Vonage SDK to make an API request to Vonage, instructing it to send an SMS from your Vonage number to the recipient.
  2. Receiving Status Updates: When the status of the sent SMS changes (e.g., submitted, delivered, failed), the Vonage platform sends an HTTP POST request containing the status details to a pre-configured Status URL (your webhook endpoint). Your Express application listens at this URL, receives the data, and processes it.

1. Setting Up the Project

Let's create the project structure 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-sms-status-app
    cd vonage-sms-status-app
  2. Initialize Node.js Project: Initialize the project using npm or yarn. This creates a package.json file.

    bash
    npm init -y
    # or
    # yarn init -y
  3. Install Dependencies: Install Express for the web server, the Vonage SDK, and dotenv for managing environment variables.

    bash
    npm install express @vonage/server-sdk dotenv
    # or
    # yarn add express @vonage/server-sdk dotenv
  4. Create Project Structure: Set up a basic source directory and necessary files.

    bash
    mkdir src
    touch src/index.js
    touch .env
    touch .gitignore
  5. Configure .gitignore: Prevent sensitive files and build artifacts from being committed to version control. Add the following to your .gitignore file:

    text
    # .gitignore
    node_modules
    .env
    private.key
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
  6. Set Up Environment Variables (.env): Create a .env file in the project root to store your Vonage credentials and configuration. Never commit this file to Git.

    dotenv
    # .env
    
    # Vonage API Credentials (Found in Vonage Dashboard > API Settings)
    # Note: API Key/Secret might be used for other Vonage APIs, but Messages API primarily uses App ID + Private Key
    VONAGE_API_KEY=YOUR_VONAGE_API_KEY
    VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
    
    # Vonage Application Credentials (Created in Vonage Dashboard > Your Applications)
    VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
    # IMPORTANT: Provide the *path* to your downloaded private key file.
    # This path is relative to the current working directory where the Node process starts.
    VONAGE_PRIVATE_KEY_PATH=./private.key
    
    # Your Vonage Virtual Number (Must be linked to the Application ID)
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    
    # Server Port
    PORT=3000
    
    # Optional: For JWT Signature Verification (Store securely, NOT directly in .env for production)
    # Example structure - store the actual key securely, e.g., in secrets manager or env var
    # VONAGE_PUBLIC_KEY_STRING=""-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----""
    • VONAGE_API_KEY, VONAGE_API_SECRET: Found at the top of your Vonage API Dashboard. While present, the Messages API primarily uses JWT authentication (Application ID + Private Key).
    • VONAGE_APPLICATION_ID: Obtained after creating a Vonage Application (see next section).
    • VONAGE_PRIVATE_KEY_PATH: The file path to the private.key file downloaded when creating the Vonage Application. Place this file in your project root or specify the correct path relative to where you run the node command. Ensure this file is readable by the Node.js process.
    • VONAGE_NUMBER: Your purchased Vonage virtual number, formatted with the country code (e.g., 14155550100).
    • PORT: The port your Express server will listen on (defaulting to 3000).

2. Configuring Vonage

To send messages and receive status updates using the Messages API, you need to create a Vonage Application and configure it correctly.

  1. Navigate to Vonage Applications: Log in to the Vonage API Dashboard and navigate to ""Your applications"" > ""Create a new application"".

  2. Create the Application:

    • Name: Give your application a descriptive name (e.g., ""SMS Status Webhook App"").
    • Generate Public and Private Key: Click this button. Your browser will download a private.key file. Save this file securely in your project directory (or another location referenced via VONAGE_PRIVATE_KEY_PATH in your .env file). Make sure this file is included in your .gitignore. Vonage stores the public key associated with this application.
    • Application ID: Note the generated Application ID. Add it to your .env file as VONAGE_APPLICATION_ID.
  3. Enable Capabilities:

    • Scroll down to the ""Capabilities"" section.
    • Toggle Messages to enable it. This reveals fields for Inbound and Status URLs.
  4. Configure Webhook URLs:

    • Status URL: This is where Vonage will send delivery status updates. For now, enter a placeholder like https://example.com/webhooks/status. We will update this later with our ngrok URL during testing. Ensure the method is set to POST.
    • Inbound URL: If you also wanted to receive SMS messages (not just status updates), you would configure this URL. We can leave it blank for this guide or use a placeholder like https://example.com/webhooks/inbound. Ensure the method is set to POST.
  5. Link Your Vonage Number:

    • Scroll down to ""Link virtual numbers"".
    • Find the Vonage number you want to use for sending SMS (the one specified in your .env file) and click ""Link"".
  6. Save Changes: Click ""Save changes"" at the bottom of the page.

  7. (Optional but Recommended) Ensure Messages API is Default: While the application settings usually suffice, you can double-check your account-level SMS settings. Go to ""Account"" > ""Settings"". Scroll to ""API settings"" > ""SMS settings"". Ensure the default API for sending SMS is set to ""Messages API"". This avoids potential conflicts if you previously used the older SMS API settings.

3. Implementing the Express Server (Webhook Handler)

Now, let's write the code for the Express server that will listen for incoming status webhooks from Vonage.

Edit src/index.js:

javascript
// src/index.js
require('dotenv').config(); // Load environment variables from .env file (searches relative to CWD)
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs'); // Needed to read the private key file

// --- Basic Server Setup ---
const app = express();
// Vonage sends webhooks with application/json content type
app.use(express.json());
// Optional: If you need to handle URL-encoded data (not typical for Vonage webhooks)
app.use(express.urlencoded({ extended: true }));

const port = process.env.PORT || 3000;

// --- Vonage Client Initialization ---
// Read the private key from the file path specified in .env
// Ensure the path is correct relative to where the node process starts.
let privateKey;
try {
    privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (err) {
    console.error(""Error reading private key file:"", err);
    process.exit(1); // Exit if key is essential and missing
}

// Initialize the Vonage SDK for the Messages API using Application ID and Private Key (JWT Auth)
// Note: Other Vonage APIs might use API Key/Secret authentication instead.
const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY, // Optional for Messages API, but good practice to include
    apiSecret: process.env.VONAGE_API_SECRET, // Optional for Messages API
    applicationId: process.env.VONAGE_APPLICATION_ID,
    privateKey: privateKey // Use the key content read from the file
});

// --- Webhook Endpoint for Status Updates ---
// This path MUST match the path configured in the Vonage Application's Status URL
app.post('/webhooks/status', (req, res) => {
    const statusData = req.body;

    console.log('--- Vonage Status Webhook Received ---');
    console.log('Timestamp:', statusData.timestamp);
    console.log('Message UUID:', statusData.message_uuid);
    console.log('Status:', statusData.status);
    console.log('To:', statusData.to);
    console.log('From:', statusData.from);

    if (statusData.error) {
        console.error('Error Code:', statusData.error.code);
        console.error('Error Reason:', statusData.error.reason);
    }

    // Store or process the status update here (e.g., update a database)
    // For this example, we just log it.
    // See Section 7 for persistence ideas.

    // IMPORTANT: Respond to Vonage with a 200 OK status code
    // This acknowledges receipt of the webhook. Failure to do so
    // will cause Vonage to retry sending the webhook.
    res.status(200).send('OK');
    // Alternatively: res.sendStatus(200);
});

// --- Basic Health Check Endpoint ---
app.get('/_health', (req, res) => {
    res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});

// --- Start the Server ---
app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
    console.log(`Webhook endpoint available at /webhooks/status (POST)`);
});

Explanation:

  1. require('dotenv').config(): Loads variables from your .env file into process.env. It looks for .env starting from the current working directory (CWD) of the Node.js process.
  2. express(): Creates an Express application instance.
  3. app.use(express.json()): Adds middleware to parse incoming requests with JSON payloads (which Vonage uses for webhooks).
  4. Reading privateKey: We use fs.readFileSync to read the private key content from the path specified in VONAGE_PRIVATE_KEY_PATH. Error handling is added in case the file doesn't exist or isn't readable.
  5. vonage = new Vonage(...): Initializes the Vonage SDK client. For the Messages API, applicationId and the content of the privateKey are essential for JWT authentication. We pass the actual key content, not the file path, to the SDK.
  6. app.post('/webhooks/status', ...): Defines the route handler for POST requests to /webhooks/status.
    • It logs the key fields from the incoming req.body (the webhook payload). Common fields include message_uuid, status (submitted, delivered, rejected, undeliverable, failed), timestamp, to, from, and an optional error object if the status is failed or rejected.
    • Crucially, it sends back a 200 OK status using res.status(200).send('OK');. This tells Vonage you've successfully received the webhook. Without this, Vonage will retry sending the webhook according to its retry schedule, potentially causing duplicate processing.
  7. app.get('/_health', ...): A simple endpoint to check if the server is running.
  8. app.listen(...): Starts the Express server on the specified port.

4. Sending an SMS to Trigger Status Updates

To test our webhook, we need to send an SMS message using the Vonage Messages API. The status updates for this message will then be sent to our webhook.

You can add a simple function to src/index.js or create a separate script. Let's create a separate script for clarity.

Create src/send-test-sms.js:

javascript
// src/send-test-sms.js
require('dotenv').config();
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs');

// Read the private key content
let privateKey;
try {
    privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (err) {
    console.error(""Error reading private key file:"", err);
    process.exit(1);
}

// Initialize Vonage SDK (same as in index.js)
const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY,
    apiSecret: process.env.VONAGE_API_SECRET,
    applicationId: process.env.VONAGE_APPLICATION_ID,
    privateKey: privateKey
});

// Replace with a valid recipient phone number
const RECIPIENT_NUMBER = ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER""; // e.g., 14155550101

async function sendSms() {
    const fromNumber = process.env.VONAGE_NUMBER;
    const toNumber = RECIPIENT_NUMBER;
    const text = `Hello from Vonage! Testing status webhook. [${new Date().toLocaleTimeString()}]`;

    if (!toNumber || toNumber === ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER"") { // Basic check
        console.error('Error: Please replace RECIPIENT_NUMBER in src/send-test-sms.js');
        process.exit(1);
    }
    if (!fromNumber) {
        console.error('Error: VONAGE_NUMBER not found in .env file.');
        process.exit(1);
    }

    console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`);

    try {
        const resp = await vonage.messages.send({
            message_type: ""text"",
            text: text,
            to: toNumber,
            from: fromNumber,
            channel: ""sms""
        });
        console.log('SMS Submitted Successfully!');
        console.log('Message UUID:', resp.message_uuid);
    } catch (err) {
        console.error('Error sending SMS:');
        // Log specific Vonage error details if available
        if (err.response && err.response.data) {
            console.error(JSON.stringify(err.response.data, null, 2));
        } else {
            console.error(err);
        }
    }
}

sendSms();

Explanation:

  1. Initializes the Vonage client similarly to index.js, including reading the private key file content.
  2. Defines the RECIPIENT_NUMBER. Remember to replace the placeholder with a real phone number you can check.
  3. Uses vonage.messages.send() to send the SMS.
    • message_type: ""text"": Specifies a plain text message.
    • text: The content of the SMS.
    • to: The recipient's phone number.
    • from: Your Vonage virtual number (from .env).
    • channel: ""sms"": Specifies the SMS channel.
  4. Logs the message_uuid upon successful submission or logs the error details if the API call fails.

Before running: Edit src/send-test-sms.js and replace ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER"" with a valid E.164 formatted phone number.

5. Running Locally with ngrok

To allow Vonage's servers to reach your local development machine, you need to use ngrok.

  1. Start ngrok: Open a new terminal window (keep the one for the server running separately). Run ngrok, telling it to forward traffic to the port your Express app is listening on (e.g., 3000).

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

    ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000 Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

    Copy the https Forwarding URL (e.g., https://xxxxxxxx.ngrok.io). Using HTTPS is strongly recommended.

  3. Update Vonage Status URL:

    • Go back to your application settings in the Vonage API Dashboard (""Your applications"" > Select your app).
    • Scroll to ""Capabilities"" > ""Messages"".
    • Paste the copied https ngrok URL into the Status URL field, appending your webhook path: https://xxxxxxxx.ngrok.io/webhooks/status.
    • Ensure the method is POST.
    • Click Save changes.

Note: ngrok provides a temporary public URL suitable for development. For production, you will need to deploy your application to a server with a permanent public IP address or domain name and configure a valid SSL/TLS certificate. See Section 10.

6. Verification and Testing

Now, let's test the end-to-end flow.

  1. Start the Express Server: In your first terminal window, run:

    bash
    node src/index.js

    You should see Server listening at http://localhost:3000.

  2. Send a Test SMS: In another terminal window (not the ngrok one), run the sending script:

    bash
    node src/send-test-sms.js

    You should see ""SMS Submitted Successfully!"" and a message_uuid.

  3. Observe Webhook Receipt:

    • Watch the terminal where your Express server (src/index.js) is running. Within a few seconds to minutes (depending on carrier networks), you should start seeing logs like ""--- Vonage Status Webhook Received ---"" followed by the status details (submitted, then potentially delivered or failed).
    • Check the recipient phone number – it should receive the SMS message.
  4. Inspect with ngrok (Optional): Open the ngrok Web Interface URL (usually http://127.0.0.1:4040) in your browser. You can inspect the incoming POST requests to /webhooks/status, view headers, the request body (payload), and your server's response (200 OK). This is invaluable for debugging.

Manual Verification Checklist:

  • Express server starts without errors (especially private key reading).
  • send-test-sms.js executes and logs a message_uuid.
  • Recipient phone receives the SMS message.
  • Express server logs show incoming requests to /webhooks/status.
  • Logged status progresses (e.g., from submitted to delivered).
  • ngrok web interface shows POST requests to /webhooks/status receiving a 200 OK response.

7. Enhancements: Persistence and Data Storage

Logging status updates to the console is fine for testing, but in a production scenario, you'll want to store this information persistently, typically in a database.

Conceptual Database Schema:

A simple table to store message statuses might look like this (using PostgreSQL syntax):

sql
CREATE TABLE message_statuses (
    message_uuid VARCHAR(36) PRIMARY KEY, -- Vonage Message UUID
    status VARCHAR(50) NOT NULL,          -- e.g., 'submitted', 'delivered', 'failed'
    recipient_number VARCHAR(20),         -- To number
    sender_number VARCHAR(20),            -- From number (Your Vonage number)
    -- Timestamp from Vonage webhook. Using TIMESTAMPTZ stores the timestamp
    -- along with time zone information (typically converting to UTC for storage),
    -- which is crucial for accurately recording event times from external systems
    -- like Vonage, regardless of server or client time zones.
    status_timestamp TIMESTAMPTZ NOT NULL,
    error_code VARCHAR(50),               -- Error code if status is 'failed'/'rejected'
    error_reason TEXT,                    -- Error reason text
    created_at TIMESTAMPTZ DEFAULT NOW(),  -- When the record was first created (optional)
    updated_at TIMESTAMPTZ DEFAULT NOW()   -- When the record was last updated
);

Implementation Steps (Conceptual):

  1. Choose a Database: Select a database (e.g., PostgreSQL, MySQL, MongoDB).
  2. Install Database Driver/ORM: Add the appropriate Node.js driver or ORM (e.g., pg for PostgreSQL, mysql2 for MySQL, mongoose for MongoDB, or a higher-level ORM like Prisma or Sequelize).
    bash
    # Example for PostgreSQL with Prisma
    npm install @prisma/client
    npm install prisma --save-dev
    npx prisma init --datasource-provider postgresql
    # Define schema in prisma/schema.prisma based on the SQL above
    # npx prisma migrate dev --name init
  3. Update Webhook Handler: Modify the /webhooks/status handler in src/index.js to:
    • Initialize your database client/ORM.
    • Inside the handler, use the incoming statusData (especially message_uuid) to find or create a record in your database table (an UPSERT operation is often ideal).
    • Update the record with the latest status, status_timestamp, and any error details. Convert the ISO 8601 timestamp string from Vonage into a Date object for storage.
    • Implement appropriate error handling for database operations. Crucially, still ensure a 200 OK is sent back to Vonage even if your database update fails, but log the database error internally for investigation. You might implement a separate retry mechanism for failed DB writes later.
javascript
// src/index.js - Conceptual Database Update in Webhook

// --- Assume Prisma Client is initialized as 'prisma' ---
// const { PrismaClient } = require('@prisma/client');
// const prisma = new PrismaClient();

app.post('/webhooks/status', async (req, res) => { // Make handler async
    const statusData = req.body;
    console.log('--- Vonage Status Webhook Received ---');
    console.log(JSON.stringify(statusData, null, 2)); // Log full payload

    try {
        // Example: Using Prisma to update the database
        await prisma.messageStatus.upsert({
            where: { message_uuid: statusData.message_uuid },
            update: {
                status: statusData.status,
                status_timestamp: new Date(statusData.timestamp), // Convert ISO string to Date object
                error_code: statusData.error?.code,
                error_reason: statusData.error?.reason,
                updated_at: new Date()
            },
            create: {
                message_uuid: statusData.message_uuid,
                status: statusData.status,
                recipient_number: statusData.to,
                sender_number: statusData.from,
                status_timestamp: new Date(statusData.timestamp), // Convert ISO string to Date object
                error_code: statusData.error?.code,
                error_reason: statusData.error?.reason,
                // created_at and updated_at might have defaults in the DB schema
            }
        });
        console.log(`Database updated for message: ${statusData.message_uuid}`);

    } catch (dbError) {
        console.error(`Database error processing status for ${statusData.message_uuid}:`, dbError);
        // Decide on internal error handling/retry strategy for DB errors
        // BUT STILL ACKNOWLEDGE THE WEBHOOK TO VONAGE
    } finally {
        // ALWAYS send 200 OK back to Vonage unless there's a catastrophic server failure
        // preventing even this response.
         res.status(200).send('OK');
    }
});

8. Error Handling and Logging

Robust error handling and logging are essential for production.

  • Webhook Handler:
    • Wrap database/processing logic in try...catch blocks.
    • Log errors comprehensively, including the message UUID and the full error stack.
    • Always return 200 OK to Vonage unless your server is fundamentally unable to process any requests. Vonage has its own retry mechanism for non-2xx responses; let it handle transient network issues. Focus your internal retries on downstream dependencies like your database if needed.
  • SMS Sending:
    • Wrap vonage.messages.send() in try...catch.
    • Log detailed errors, including potential response data from the Vonage API (err.response.data in Axios-based errors from the SDK).
  • Structured Logging: Use a dedicated logging library like Winston or Pino for structured logging (e.g., JSON format). This makes logs easier to parse and analyze in production.
javascript
// Example using Winston (conceptual setup)
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info', // Control log level via env var
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json() // Use JSON format for structured logs
  ),
  defaultMeta: { service: 'sms-status-service' },
  transports: [
    // In development, log to console with simple format
    // In production, log to console (to be captured by container orchestrator)
    // or configure file/remote transports
    new winston.transports.Console({
      format: process.env.NODE_ENV !== 'production'
        ? winston.format.simple()
        : winston.format.json(), // Use simple format locally, JSON in prod
    }),
  ],
});

// Replace console.log/console.error with logger.info/logger.error
// e.g., logger.info('Webhook received', { messageId: statusData.message_uuid, status: statusData.status });
// e.g., logger.error('Database update failed', { messageId: statusData.message_uuid, error: dbError.message, stack: dbError.stack });

9. Security Considerations

Protecting your webhook endpoint and credentials is vital.

  • Environment Variables: Never hardcode credentials. Use .env locally and secure environment variable management (like platform secrets or a secrets manager) in your deployment environment. Ensure .env and private.key are in .gitignore.

  • Private Key Handling: Treat your private.key file as highly sensitive. Ensure its permissions are restricted. In production deployments (especially containers), avoid copying the key file directly into the image; use secure methods like mounted volumes or secrets management tools (Docker secrets, Kubernetes secrets, cloud provider secrets managers).

  • Webhook Signature Verification (Highly Recommended): The Vonage Messages API supports signing webhooks with JWT (JSON Web Tokens). This allows your application to verify that incoming requests genuinely originated from Vonage.

    1. Enable Signed Webhooks: In your Vonage Application settings under Messages > Webhook security, select ""Enable signed webhooks"" and choose ""JWT (recommended)"". Vonage will display the Public Key associated with your application.
    2. Store Public Key Securely: Obtain the Public Key string from the Vonage dashboard. Do not hardcode the public key directly in your source code. Store it securely, for example, as an environment variable or retrieve it from a secrets management service at runtime.
    3. Install Verification Library:
      bash
      npm install jsonwebtoken
      # or
      # yarn add jsonwebtoken
    4. Implement Verification Middleware: Use the jsonwebtoken library and the Vonage public key to verify the Authorization: Bearer <token> header sent with each webhook.
    javascript
    // Conceptual JWT Verification Middleware
    const jwt = require('jsonwebtoken');
    
    // Load the public key securely (e.g., from environment variable)
    // IMPORTANT: Avoid hardcoding this in production code. Load from secure source.
    const VONAGE_PUBLIC_KEY = process.env.VONAGE_PUBLIC_KEY_STRING;
    // Example format: ""-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----""
    
    if (!VONAGE_PUBLIC_KEY) {
        // Use your logger here
        console.warn(""VONAGE_PUBLIC_KEY environment variable not set. Skipping JWT verification. THIS IS INSECURE FOR PRODUCTION."");
    }
    
    function verifyVonageSignature(req, res, next) {
        // Only verify if the public key was loaded
        if (!VONAGE_PUBLIC_KEY) {
            return next(); // Skip verification if key is missing (warning already logged)
        }
    
        const authHeader = req.headers.authorization;
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            // Use your logger here
            console.warn('Missing or invalid Authorization header for JWT verification.');
            return res.sendStatus(401); // Unauthorized
        }
        const token = authHeader.split(' ')[1];
    
        try {
            // Verify the token using the application's public key
            // Ensure correct algorithm (RS256) which Vonage uses for Messages API JWT
            const decoded = jwt.verify(token, VONAGE_PUBLIC_KEY, { algorithms: ['RS256'] });
    
            // Optional but Recommended: Check if decoded application_id matches your app ID
            if (decoded.application_id !== process.env.VONAGE_APPLICATION_ID) {
                 // Use your logger here
                 console.warn('JWT application_id mismatch', { jwtAppId: decoded.application_id, expectedAppId: process.env.VONAGE_APPLICATION_ID });
                 return res.sendStatus(401); // Or 403 Forbidden
            }
    
            // Attach decoded payload if needed for auditing, or just proceed
            req.vonage_jwt = decoded; // Example: make decoded token available
            // Use your logger here
            console.info('JWT signature verified successfully', { messageId: req.body?.message_uuid });
            next(); // Signature is valid, proceed to the handler
        } catch (err) {
            // Use your logger here
            console.error('Invalid JWT signature', { error: err.message, tokenReceived: token ? 'yes' : 'no' });
            return res.sendStatus(401); // Unauthorized - signature verification failed
        }
    }
    
    // Apply middleware *before* your route handler in src/index.js
    // Ensure logger is defined if using it within the middleware
    app.post('/webhooks/status', verifyVonageSignature, async (req, res) => {
        // ... existing handler logic ...
    });
  • HTTPS: Always use HTTPS for your webhook endpoint in production. Ensure your server has a valid SSL/TLS certificate configured.

  • Input Validation: Even with JWT verification, sanitize and validate expected fields within the webhook payload before using them (e.g., check data types, lengths, expected values for status).

  • Rate Limiting: Implement rate limiting on your webhook endpoint using middleware like express-rate-limit to prevent abuse.

Frequently Asked Questions

how to track sms delivery status with vonage

Track SMS delivery status by setting up a webhook endpoint with the Vonage Messages API. Your Node.js application will receive real-time status updates (e.g., delivered, failed) via this endpoint, enabling you to implement custom logic based on these updates, such as retry mechanisms or user notifications. This guide provides a comprehensive walkthrough of the process using Express.js and the Vonage Server SDK.

what is a vonage status webhook

A Vonage status webhook is an HTTP endpoint you provide to the Vonage Messages API. When the status of an SMS message changes (e.g., sent, delivered, failed), Vonage sends an HTTP POST request to this URL with status details. This allows your application to react to delivery events in real-time.

why use vonage messages api for sms

The Vonage Messages API offers a multi-channel approach for sending and receiving various message types, including SMS. It provides features like delivery status updates via webhooks, allowing for robust error handling and improved communication workflows. The API simplifies sending SMS messages from your Node.js applications.

when to configure vonage status url

Configure the Vonage Status URL when creating or modifying a Vonage Application in the Vonage API Dashboard. This URL is essential for receiving real-time delivery receipts and handling potential message failures. It must point to a publicly accessible endpoint on your server where you'll process the incoming status updates.

how to create a vonage application for sms

Create a Vonage Application by logging into the Vonage API Dashboard, navigating to 'Your Applications,' and clicking 'Create a new application.' Provide a descriptive name, generate public and private keys (securely storing the private key), and enable the 'Messages' capability under the capabilities section. Link the application to your Vonage Virtual Number, allowing you to send and receive messages through the Vonage Messages API using JWT Authentication.

what is vonage private key path

The `VONAGE_PRIVATE_KEY_PATH` environment variable stores the *file path* to your downloaded `private.key` file, relative to where your Node.js process starts. This key is crucial for authenticating with the Vonage Messages API and should *never* be hardcoded or exposed in version control. The file's contents are used with your application ID for JWT authentication with the Vonage server SDK.

how to send test sms with vonage api

Send a test SMS using the Vonage Messages API by initializing the Vonage Node.js SDK with your credentials and calling `vonage.messages.send()`. Provide the recipient's number, your Vonage virtual number, and the message text. Ensure `RECIPIENT_NUMBER` in the `send-test-sms.js` example is replaced with a valid number.

what is ngrok used for with vonage webhooks

ngrok creates a temporary public URL that tunnels to your local development server. This allows Vonage to send webhook requests to your local machine during development, even though it's behind a firewall or NAT. Ngrok is essential for testing webhooks locally, ensuring they function correctly before production deployment.

how to handle vonage webhook errors

Handle Vonage webhook errors by implementing robust error handling within your webhook route handler using `try...catch` blocks, logging errors with details such as message UUID and error stack, and *always returning a 200 OK status to Vonage*. This prevents Vonage from repeatedly retrying the webhook and allows your application to manage any issues with downstream services, like database updates, separately.

why respond with 200 ok to vonage webhook

Responding with a 200 OK status to a Vonage webhook acknowledges successful receipt of the webhook data. Without a 200 OK response, Vonage assumes the webhook failed and will retry sending it, potentially leading to duplicate processing. This is crucial for reliable communication between your application and the Vonage Messages API.

what database to use for vonage sms statuses

You can choose a database suitable for your needs and resources, such as PostgreSQL, MySQL, MongoDB, or others, to persist your Vonage SMS delivery statuses. Consider data volume, query complexity, and ease of integration with Node.js when selecting a database. You may also consider an ORM such as Prisma or Sequelize for database interactions.

how to secure vonage webhook endpoint

Secure your Vonage webhook endpoint by using HTTPS, verifying JWT signatures, managing credentials securely (especially your private key and public key), validating input data, and implementing rate limiting to prevent abuse. Never hardcode credentials or expose sensitive information in your codebase.

can I verify vonage webhook signatures

Yes, you can verify Vonage webhook signatures using JWT (JSON Web Tokens). Enable signed webhooks in your Vonage application settings and store the public key securely to validate signatures. The example uses the `jsonwebtoken` library for verification, which helps ensure the integrity and authenticity of incoming webhook requests.

what is vonage application id

The Vonage Application ID is a unique identifier assigned to your Vonage application. It's used along with your private key for JWT authentication with various Vonage APIs, especially when interacting with the Messages API. This method is preferred over API Key/Secret authentication in modern Vonage APIs.