code examples

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

Vonage SMS Delivery Status Callbacks: Complete Node.js + Express Guide

Step-by-step guide to sending SMS with Vonage Messages API and receiving delivery status webhooks using Node.js, Express, and ngrok. Includes JWT webhook verification, error handling, and production deployment.

Developer guide: Sending SMS and receiving delivery status callbacks with Node.js, Express, and Vonage

Build a Node.js application using Express to send SMS messages via the Vonage Messages API and receive delivery status webhooks with real-time tracking.

What are delivery status callbacks? When you send an SMS, you need to know whether it reached the recipient. Delivery status callbacks (also called Delivery Receipt Reports or DLRs) are HTTP POST requests Vonage sends to your server containing the message status – delivered, failed, rejected, or other states. Without these callbacks, you're sending messages blind with no confirmation of successful delivery.

Why do you need them? Track delivery success rates, alert customers when critical messages fail, implement retry logic for failed sends, maintain audit logs for compliance, and provide users with real-time delivery feedback.

Project Overview and Goals

Build a Node.js application that demonstrates 2 core functionalities:

  1. Sending an SMS Message: Programmatically send an SMS message to a specified phone number using the Vonage Messages API.
  2. Receiving Delivery Status Updates: Set up a webhook endpoint to receive real-time status updates (e.g., delivered, failed, rejected) for the messages you send.

Track delivery success for critical notifications like password resets, 2FA codes, appointment reminders, order confirmations, and payment alerts where knowing the delivery status directly impacts your application's reliability and user experience.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express: A minimal and flexible Node.js web application framework for creating the webhook endpoint.
  • Vonage Messages API: A unified API for sending messages across various channels, including SMS. Use it for its robustness and multi-channel capabilities.
  • @vonage/server-sdk: The official Vonage Node.js SDK (version 3.24.1 as of January 2025) for interacting with the API.
  • dotenv: A module to load environment variables from a .env file for secure credential management.
  • ngrok: A tool to expose local development servers to the internet, necessary for testing webhooks during local development. Production deployments use your server's public URL.

System Architecture:

+-----------------+ +---------------------+ +-----------------+ | Your Node.js App|----->| Vonage Messages API |----->| Carrier Network |-----> User's Phone | (Express Server)| | (sends SMS) | | | | - Send Script | +---------------------+ +-------+---------+ | - Webhook Ep. |<-----+ (status update) (delivery report) | +-----------------+ | | ^ | | | +------------------------------------+ | (ngrok tunnel for local dev) +-------+---------+ | ngrok Service | +-----------------+

(Note: For published documentation, replace this ASCII diagram with an image for consistent rendering across platforms.)

Final Outcome:

By the end of this guide, you'll have a functional Node.js application capable of:

  • Sending an SMS message via a simple script.
  • Running an Express server with a webhook endpoint (/webhooks/status).
  • Receiving and logging delivery status updates sent by Vonage to your webhook.

Expected time: 45–60 minutes for developers familiar with Node.js and Express. If you're new to webhooks or SMS APIs, allow 90 minutes.

Prerequisites:

  • Node.js and npm: Node.js 18.x or higher (LTS version recommended). Download Node.js
  • Vonage API Account: Sign up for a free account. Vonage Signup
    • You need your API Key and API Secret.
    • You need to create a Vonage Application and get its Application ID and generate/download a Private Key.
    • You need at least 1 Vonage virtual phone number capable of sending SMS.
  • ngrok: Install and authenticate. Download ngrok (A free account is sufficient).
  • A personal phone number to receive test SMS messages.

Pricing: Vonage charges per SMS sent (~$0.0070–$0.15 depending on destination country). New accounts receive free trial credit ($2.00 as of January 2025). Check current pricing at Vonage SMS Pricing.


1. Node.js Project Setup and Dependency Installation

Initialize the project, install dependencies, and set up the basic structure.

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

    bash
    mkdir vonage-sms-status-guide
    cd vonage-sms-status-guide
  2. Initialize Node.js Project: Initialize npm to create a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install express for the web server, @vonage/server-sdk to interact with the Vonage API, and dotenv to manage environment variables.

    bash
    npm install express @vonage/server-sdk dotenv

    Note: This installs @vonage/server-sdk version 3.24.1 (latest as of January 2025). This version uses TypeScript for improved code completion and follows modern async/await patterns.

  4. Create Project Structure: Create the necessary files and directories.

    bash
    touch index.js send-sms.js .env .gitignore
    • index.js: Main file for the Express server (webhook listener).
    • send-sms.js: Script to trigger sending an SMS message.
    • .env: Stores sensitive credentials (API keys, etc.).
    • .gitignore: Specifies files/directories Git should ignore.
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing dependencies and sensitive credentials.

    text
    # .gitignore
    node_modules/
    .env
    private.key # Or your actual private key filename

    Note: Add private.key assuming you might save the downloaded key file directly in the project root during development.

    Warning: While convenient for local development, storing private keys directly in the project folder carries risks. Never commit your private key file to version control (Git). Ensure .gitignore correctly lists the key file name.

  6. Configure .env File: Open the .env file and add placeholders for your Vonage credentials. Fill these in later.

    dotenv
    # .env
    VONAGE_API_KEY=YOUR_API_KEY_HERE
    VONAGE_API_SECRET=YOUR_API_SECRET_HERE
    VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE
    VONAGE_PRIVATE_KEY_PATH=./private.key # Ensure this matches the actual path and filename of your downloaded private key
    VONAGE_NUMBER=14155552671 # Your Vonage virtual number in E.164 format (country code + number, no spaces or symbols)
    
    # Add the recipient number for testing the send script
    TO_NUMBER=14155559876 # Recipient phone number in E.164 format
    • Replace YOUR_API_KEY_HERE, YOUR_API_SECRET_HERE, YOUR_APPLICATION_ID_HERE, and the phone numbers with your actual values.
    • E.164 format: Country code (1 for US/Canada) followed by area code and number with no spaces, dashes, or parentheses. Example: 14155552671 for a US number.

2. Vonage Account Configuration and API Credentials

Configure your Vonage account, create an application, and obtain the necessary credentials.

  1. Log in to Vonage Dashboard: Access your Vonage API Dashboard.

  2. Find API Key and Secret: Your API Key and Secret appear prominently on the main dashboard page. Copy these values and paste them into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.

  3. Set Default SMS API: Ensure your account uses the Messages API for sending SMS, as this guide relies on it.

    • Navigate to Account Settings in the left sidebar.
    • Scroll to the API Settings section.
    • Under Default SMS Setting, select Messages API.
    • Click Save changes.
    • Why? Vonage has legacy APIs. Selecting Messages API ensures consistency in sending behavior and webhook formats used in this guide.
  4. Create a Vonage Application: The Messages API requires an application context for authentication using a private key.

    • Navigate to Applications in the left sidebar.
    • Click + Create a new application.
    • Give your application a name (e.g., SMS Status Guide App).
    • Click Generate public and private key. This automatically downloads the private.key file. Save this file securely. For development, place it in your project's root directory (as configured in .env). Vonage typically names this private.key – ensure the path you set in .env matches the exact filename and location where you saved it.
    • Security Note: Placing the key in the project root is shown for development simplicity. For production, store keys securely outside your codebase (e.g., using secrets management tools) and ensure strict file permissions.
    • What is the private key for? Vonage uses JWT (JSON Web Token) authentication for the Messages API. Your application signs API requests with the private key to prove its identity. The corresponding public key (stored in Vonage's system) verifies these signatures. This is more secure than sending API credentials with every request.
    • Enable the Messages capability.
    • You'll see fields for Status URL and Inbound URL. Fill these later when you have your ngrok URL. For now, leave them blank or enter temporary placeholders like http://example.com/status.
    • Click Generate new application.
    • You'll be taken to the application details page. Copy the Application ID and paste it into your .env file for VONAGE_APPLICATION_ID.
  5. Link Your Vonage Number: Link your Vonage virtual number to the application you just created.

    • On the application details page, scroll to the Link virtual numbers section.
    • Find your Vonage number in the list and click the Link button next to it.
    • Copy your Vonage virtual number (in E.164 format) and paste it into your .env file for VONAGE_NUMBER.
    • Don't have a number? Navigate to Numbers > Buy numbers in the left sidebar. Search for available numbers in your country. Purchase or rent a number ($0.90/month for US numbers as of January 2025). Ensure the number supports SMS capability.
  6. Update .env with Private Key Path: Ensure VONAGE_PRIVATE_KEY_PATH in your .env file correctly points to where you saved the downloaded private key file (e.g., ./private.key if it's in the root and named private.key). Match the actual filename if it differs.

Your .env file should now contain your actual credentials (except for the webhook URLs, handled later).


3. Implementing SMS Sending with Vonage Messages API

Write the script to send an SMS message using the Vonage Node.js SDK and the Messages API.

  1. Edit send-sms.js: Open the send-sms.js file and add the following code:

    javascript
    // send-sms.js
    require('dotenv').config(); // Load environment variables from .env file
    
    const { Vonage } = require('@vonage/server-sdk');
    const { Auth } = require('@vonage/auth');
    
    // --- Configuration ---
    const VONAGE_API_KEY = process.env.VONAGE_API_KEY;
    const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET;
    const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID;
    const VONAGE_PRIVATE_KEY_PATH = process.env.VONAGE_PRIVATE_KEY_PATH;
    const VONAGE_NUMBER = process.env.VONAGE_NUMBER;
    const TO_NUMBER = process.env.TO_NUMBER; // Recipient number from .env
    
    // Input validation basic check
    if (!VONAGE_API_KEY || !VONAGE_API_SECRET || !VONAGE_APPLICATION_ID || !VONAGE_PRIVATE_KEY_PATH || !VONAGE_NUMBER || !TO_NUMBER) {
        console.error("Error: Missing required environment variables. Check your .env file.");
        process.exit(1); // Exit if configuration is missing
    }
    
    // --- Initialize Vonage ---
    // Use Application ID and Private Key for authentication with Messages API
    const credentials = new Auth({
        apiKey: VONAGE_API_KEY,
        apiSecret: VONAGE_API_SECRET,
        applicationId: VONAGE_APPLICATION_ID,
        privateKey: VONAGE_PRIVATE_KEY_PATH,
    });
    const options = {}; // Optional: Add any client options here if needed
    const vonage = new Vonage(credentials, options);
    
    // --- Send SMS Function ---
    async function sendSms(textMessage) {
        console.log(`Attempting to send SMS from ${VONAGE_NUMBER} to ${TO_NUMBER}`);
        try {
            const resp = await vonage.messages.send({
                message_type: "text",
                text: textMessage,
                to: TO_NUMBER,
                from: VONAGE_NUMBER,
                channel: "sms"
            });
            console.log(`SMS submitted successfully! Message UUID: ${resp.messageUuid}`);
            return resp.messageUuid; // Return the ID for potential tracking
        } catch (err) {
            console.error("Error sending SMS:");
            // Log the detailed error response if available
            if (err.response && err.response.data) {
                 console.error(JSON.stringify(err.response.data, null, 2));
            } else {
                console.error(err);
            }
            throw err; // Re-throw the error for upstream handling if needed
        }
    }
    
    // --- Execute Sending ---
    // Immediately invoke the function when the script runs
    (async () => {
        const messageContent = `Hello from Vonage! Sent at: ${new Date().toLocaleTimeString()}`;
        try {
            await sendSms(messageContent);
            console.log("Script finished.");
        } catch (error) {
            console.error("Script failed to send SMS.");
            process.exit(1); // Exit with error code if sending failed
        }
    })(); // IIFE (Immediately Invoked Function Expression) to run async code
  2. Code Explanation:

    • require('dotenv').config(): Loads variables from the .env file into process.env.
    • Configuration: Retrieves necessary credentials and numbers from process.env. Includes basic validation.
    • Initialize Vonage:
      • Use Auth with applicationId and privateKey – this is the required authentication method for sending via the Messages API when an application context is needed (standard practice). While the API Key/Secret are included in the Auth object, the Application ID and Private Key primarily drive authentication for this Messages API operation; the Key/Secret might be leveraged by other SDK functionalities or legacy API interactions.
      • new Vonage(credentials, options) creates the Vonage client instance.
    • sendSms Function:
      • An async function handles the asynchronous API call.
      • vonage.messages.send({...}): The core method call.
        • message_type: "text": Specifies a standard text message.
        • text: The SMS content. Maximum 160 characters for single-segment SMS using GSM-7 encoding. Longer messages split into multiple segments (up to 1,530 characters across 10 segments). Using Unicode characters reduces limit to 70 characters per segment.
        • to: The recipient's phone number (from .env).
        • from: Your Vonage virtual number (from .env).
        • channel: "sms": Explicitly defines the channel.
      • Success: Logs the messageUuid – a unique identifier (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) that tracks this specific message through its lifecycle. Store this UUID to correlate webhook status updates with sent messages.
      • Error Handling: Uses a try...catch block to catch errors during the API call. Logs detailed error information if available in the Vonage response (err.response.data).
    • Execute Sending: An IIFE runs the async sendSms function immediately when you execute the script (node send-sms.js).

4. Building Express Webhook Endpoint for Delivery Status Callbacks

Create the Express server and the webhook endpoint (/webhooks/status) to receive delivery status updates from Vonage.

  1. Edit index.js: Open index.js and add the following code for the Express server:

    javascript
    // index.js
    require('dotenv').config(); // Load environment variables first
    const express = require('express');
    
    const app = express();
    const PORT = process.env.PORT || 3000; // Use environment port or default to 3000
    
    // --- Middleware ---
    // Vonage sends webhooks as JSON
    app.use(express.json());
    // Optional: Handle URL-encoded data if needed, though status webhooks are usually JSON
    app.use(express.urlencoded({ extended: true }));
    
    // --- Webhook Endpoint for Delivery Status ---
    // Vonage POSTs status updates to this route
    app.post('/webhooks/status', (req, res) => {
        const statusUpdate = req.body;
        console.log("--- Received Status Update ---");
    
        // Log the entire payload for inspection
        console.log(JSON.stringify(statusUpdate, null, 2));
    
        // Extract key information
        const { message_uuid, status, timestamp, to, from, error } = statusUpdate;
    
        console.log(`Status for message ${message_uuid}: ${status}`);
        if (timestamp) console.log(`Timestamp: ${timestamp}`);
        if (to) console.log(`To: ${to}`);
        if (from) console.log(`From: ${from}`);
    
        // Handle specific statuses
        if (status === 'delivered') {
            console.log("Message successfully delivered!");
            // IMPLEMENTATION POINT: Update your application state (e.g., database)
        } else if (status === 'failed' || status === 'rejected') {
            console.error(`Message delivery failed or rejected.`);
            if (error) {
                console.error(`Reason: Code ${error.code}${error.reason}`);
            }
            // IMPLEMENTATION POINT: Trigger alerts or retry logic if applicable
        } else {
            console.log(`Received status: ${status}`);
        }
    
        // --- IMPORTANT: Respond to Vonage ---
        // Always send a 2xx status code quickly to acknowledge receipt.
        // Failure to do so causes Vonage to retry the webhook delivery.
        res.status(200).send('OK');
        // Alternatively use res.status(204).send(); for "No Content" response
    });
    
    // --- Basic Health Check Endpoint ---
    app.get('/health', (req, res) => {
        res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
    });
    
    
    // --- Start Server ---
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
        console.log(`Webhook endpoint available at /webhooks/status`);
        console.log(`Health check available at /health`);
        console.log("Waiting for status updates from Vonage…");
    });
  2. Code Explanation:

    • Initialization: Sets up Express and defines the port.
    • Middleware: Express processes middleware in order from top to bottom. Place express.json() before route definitions to ensure request bodies parse correctly before handlers execute.
      • express.json(): Parses incoming JSON request bodies (Vonage status webhooks use JSON).
      • express.urlencoded(): Parses URL-encoded bodies (less common for status webhooks but included for completeness).
    • /webhooks/status Endpoint (POST):
      • This is the route Vonage sends status updates to.
      • req.body: Contains the JSON payload from Vonage.
      • Logging: Logs the entire payload and key fields like message_uuid, status, timestamp – crucial for debugging.
      • Status Handling: Includes example if/else if blocks demonstrating how you might react to different statuses (delivered, failed, rejected). Integrate with your application's logic here (e.g., updating a database). The IMPLEMENTATION POINT comments highlight these areas.
      • Response (res.status(200).send('OK')): Critically important. You must respond with a 2xx status code (like 200 or 204) quickly. If Vonage doesn't receive a timely 2xx response, it assumes delivery failed and retries sending the webhook, leading to duplicate processing.
    • /health Endpoint (GET): A simple endpoint to check if the server is running.
    • Server Start: Starts the Express server listening on the specified port.

Complete webhook payload structure:

json
{
  "message_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "to": "14155559876",
  "from": "14155552671",
  "timestamp": "2025-01-15T18:30:45.123Z",
  "status": "delivered",
  "channel": "sms",
  "message_type": "text",
  "client_ref": "optional-reference-you-set",
  "usage": {
    "price": "0.0070",
    "currency": "EUR"
  },
  "error": {
    "code": 10,
    "reason": "Illegal sender address - rejected"
  }
}

Timeout: Vonage waits 10 seconds for your webhook endpoint to respond with a 2xx status code. If your endpoint doesn't respond within 10 seconds, Vonage retries the webhook. Implement retry logic as described in Section 6.


5. Local Development with ngrok Tunneling for Webhook Testing

To receive webhooks from Vonage on your local machine, expose your local server to the internet using ngrok.

  1. Start Your Local Server: Open a terminal in your project directory and run:

    bash
    node index.js

    You should see "Server listening on port 3000…" (or your configured port).

  2. Start ngrok: Open a second terminal window (leave the first one running your server). Run ngrok to tunnel traffic to your local server's port (e.g., 3000).

    bash
    ngrok http 3000
  3. Get ngrok URL: ngrok displays output similar to this:

    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 https://<random-string>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

    Copy the https://<random-string>.ngrok-free.app URL. This is your public webhook URL.

  4. Update Vonage Application Status URL:

    • Go back to your Vonage Application settings in the dashboard.
    • Find the application you created earlier (SMS Status Guide App).
    • Click the Edit button.
    • In the Capabilities section, find the Messages capability.
    • Paste your full ngrok URL into the Status URL field, appending /webhooks/status. It should look like: https://<random-string>.ngrok-free.app/webhooks/status
    • (Optional but recommended): Set the Inbound URL to https://<random-string>.ngrok-free.app/webhooks/inbound if you plan to handle incoming SMS replies later (requires adding a /webhooks/inbound route in index.js).
    • Scroll down and click Save changes.
  5. Verification: Send a Test SMS:

    • Go back to the terminal where your node index.js server is not running.

    • Run the send script again:

      bash
      node send-sms.js
    • Note the messageUuid logged by the script.

  6. Observe Webhook:

    • Watch the terminal where your node index.js server is running.
    • Within a few seconds to minutes (depending on the carrier), you should see the "--- Received Status Update ---" log message, followed by the JSON payload from Vonage.
    • Verify that the message_uuid in the webhook payload matches the one logged by send-sms.js.
    • Check the status field (e.g., submitted, delivered, failed). You might receive multiple updates for a single message as it progresses.
  7. Inspect with ngrok Web Interface: Open the ngrok Web Interface (usually http://127.0.0.1:4040) in your browser to see incoming requests to your tunneled endpoint in real-time, inspect headers, and replay requests for debugging.

Troubleshooting ngrok connection issues:

  • "ngrok not found": Ensure ngrok is installed and in your system PATH. Run ngrok version to verify.
  • "Failed to listen on localhost:3000": Your Express server isn't running. Start it with node index.js in a separate terminal.
  • "Account limit reached": Free ngrok accounts allow 1 simultaneous tunnel. Close other ngrok instances.
  • "Connection refused": Check firewall settings. Ensure port 3000 isn't blocked.

6. Production-Ready Error Handling and Retry Logic

While basic logging is in place, enhance it for production:

Implement error handling middleware:

javascript
// Add this after all routes in index.js
app.use((err, req, res, next) => {
    console.error('Unhandled error:', err);
    res.status(500).json({ error: 'Internal server error' });
});

Robust Logging: Replace console.log with a structured logging library like Winston or Pino. Log levels (info, warn, error), timestamps, and request IDs make debugging easier. Log status updates to a persistent store or logging service.

Webhook Retries: Vonage retries webhook delivery using this policy:

  • Timeout: 10 seconds for your endpoint to respond with 2xx
  • Retry attempts: Up to 10 retries over 72 hours
  • Retry intervals: 1 minute, 5 minutes, 30 minutes, 1 hour, 4 hours, 8 hours, 12 hours, 24 hours (exponential backoff)

Ensure your endpoint responds quickly. If your processing logic is slow, acknowledge the request immediately (res.status(200).send()) and perform the processing asynchronously (e.g., using a job queue like BullMQ or Redis Queue).

Idempotency: Design your webhook handler to be idempotent. If Vonage retries a webhook (e.g., due to a temporary network issue), your handler might receive the same status update multiple times. Use the message_uuid and potentially the timestamp or a unique webhook attempt ID (if provided by Vonage) to ensure you don't process the same update twice. Store processed message_uuid+status combinations temporarily if needed.

Example idempotency check:

javascript
const processedUpdates = new Set(); // In production, use Redis or database

app.post('/webhooks/status', (req, res) => {
    const { message_uuid, status, timestamp } = req.body;
    const updateKey = `${message_uuid}-${status}-${timestamp}`;

    if (processedUpdates.has(updateKey)) {
        console.log('Duplicate webhook, ignoring');
        return res.status(200).send('OK');
    }

    processedUpdates.add(updateKey);
    // Process webhook…
});

7. Webhook Security: JWT Signature Verification and Best Practices

Securing your webhook endpoint is crucial:

  • Webhook Signature Verification (Highly Recommended): Vonage uses JWT (JSON Web Token) Bearer Authorization with HMAC-SHA256 to sign webhook requests, allowing you to verify they originated from Vonage.

    How Vonage Webhook Security Works:

    1. JWT in Authorization Header: Each webhook includes a JWT in the Authorization: Bearer <token> header
    2. Signature Secret: Available in your Vonage Application settings – navigate to your application in the dashboard, and you'll find the Signature Secret in the application details page. This shared secret verifies the JWT.
    3. Three-Layer Validation:
      • JWT Signature Verification: Decode the JWT using your signature secret to confirm authenticity
      • Payload Hash Check: The JWT contains a payload_hash claim (SHA-256 hash of the webhook body). Hash the incoming req.body and compare to this claim to detect tampering
      • Timestamp Validation: The iat (issued at) claim is a UTC Unix timestamp. Compare to current time to reject stale/replayed tokens

    Implementation Example:

    javascript
    const jwt = require('jsonwebtoken');
    const crypto = require('crypto');
    
    function verifyVonageWebhook(req, signatureSecret) {
        try {
            // Extract JWT from Authorization header
            const token = req.headers.authorization?.replace('Bearer ', '');
            if (!token) return false;
    
            // Verify JWT signature and decode
            const decoded = jwt.verify(token, signatureSecret, { algorithms: ['HS256'] });
    
            // Validate payload hash
            const payloadHash = crypto
                .createHash('sha256')
                .update(JSON.stringify(req.body))
                .digest('hex');
    
            if (decoded.payload_hash !== payloadHash) {
                console.error('Payload hash mismatch');
                return false;
            }
    
            // Check timestamp freshness (reject if older than 5 minutes)
            const currentTime = Math.floor(Date.now() / 1000);
            if (currentTime - decoded.iat > 300) {
                console.error('Token too old');
                return false;
            }
    
            return true;
        } catch (error) {
            console.error('Webhook verification failed:', error.message);
            return false;
        }
    }
    
    // In your webhook handler:
    app.post('/webhooks/status', (req, res) => {
        const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET; // Add to .env
    
        if (!verifyVonageWebhook(req, signatureSecret)) {
            return res.status(401).send('Unauthorized');
        }
    
        // Process webhook…
        const statusUpdate = req.body;
        // … rest of handler
    });

    Note: Install jsonwebtoken package: npm install jsonwebtoken

  • HTTPS: ngrok provides HTTPS URLs, and your production deployment must use HTTPS to protect data in transit.

  • IP Allowlisting: As an additional layer (or alternative if signature verification isn't feasible), configure your firewall or load balancer to only allow requests to your webhook endpoint from Vonage's known IP address ranges. Find current IP ranges at Vonage IP Ranges Documentation.

  • Input Sanitization: Although the data comes from Vonage, sanitize any data from the webhook before using it in database queries or other sensitive operations to prevent potential injection attacks if the data format unexpectedly changes.

  • Rate Limiting: Implement rate limiting on your webhook endpoint (e.g., using express-rate-limit) to prevent potential abuse or denial-of-service if your endpoint is exposed accidentally.

Rate limiting example:

javascript
const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
    windowMs: 1 * 60 * 1000, // 1 minute
    max: 100, // Limit each IP to 100 requests per windowMs
    message: 'Too many requests from this IP'
});

app.post('/webhooks/status', webhookLimiter, (req, res) => {
    // Handler code…
});
  • Environment Variables: Never hardcode credentials. Use environment variables managed securely in your deployment environment.

8. Database Schema Design for SMS Message Tracking

For production use, store SMS details and status updates.

  • Why Store Data? Track message history, auditing, analytics, provide status feedback to users, handle retries based on failure codes.

  • Example Schema (Conceptual):

    sql
    CREATE TABLE sms_messages (
        message_uuid VARCHAR(255) PRIMARY KEY, -- Vonage Message UUID
        vonage_number VARCHAR(20) NOT NULL,
        recipient_number VARCHAR(20) NOT NULL,
        message_content TEXT,
        submitted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
        last_status VARCHAR(50),
        last_status_timestamp TIMESTAMPTZ,
        final_status VARCHAR(50), -- "delivered", "failed", "expired", etc.
        error_code VARCHAR(50),
        error_reason TEXT,
        created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_recipient (recipient_number),
        INDEX idx_status (last_status),
        INDEX idx_submitted (submitted_at)
    );
    
    CREATE TABLE sms_status_updates (
        id SERIAL PRIMARY KEY,
        message_uuid VARCHAR(255) REFERENCES sms_messages(message_uuid),
        status VARCHAR(50) NOT NULL,
        status_timestamp TIMESTAMPTZ NOT NULL,
        raw_payload JSONB, -- Store the full webhook payload
        received_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_message_uuid (message_uuid),
        INDEX idx_received (received_at)
    );

Performance considerations:

  • Add indexes on frequently queried columns: recipient_number, last_status, submitted_at, message_uuid
  • Use JSONB for raw_payload to enable efficient querying of webhook data
  • Set appropriate VARCHAR lengths to optimize storage

Data retention: Implement a cleanup strategy to archive or delete old records:

  • Archive messages older than 90 days to separate cold storage

  • Delete status updates for archived messages after 1 year

  • Use scheduled jobs (cron or database triggers) for automated cleanup

  • Implementation:

    • Use an ORM like Prisma or Sequelize to manage the schema and interactions.
    • When sending (send-sms.js): Insert a new record into sms_messages with the message_uuid and initial details.
    • In the webhook handler (index.js):
      • Find the corresponding record in sms_messages using message_uuid.
      • Insert a new record into sms_status_updates.
      • Update the last_status, last_status_timestamp, and potentially final_status, error_code, error_reason in the sms_messages table.

9. Handling Special SMS Delivery Cases and Edge Scenarios

  • Status Codes: Familiarize yourself with Vonage's delivery status codes (submitted, delivered, failed, expired, rejected, accepted, unknown, etc.). Handle failed and rejected specifically, potentially logging the error.code and error.reason provided in the webhook. Consult the official Vonage documentation for detailed explanations of all status and error codes.
  • Message Concatenation: Long SMS messages split into multiple parts but share the same message_uuid. Your status updates might reflect individual parts or the message as a whole depending on the carrier and Vonage processing.
  • Carrier Delays: Delivery receipts can be delayed or, in some rare cases (specific countries/carriers), not supported or provided. Your application should handle potential timeouts or lack of a final "delivered" status.
  • Timestamp Timezones: Webhook timestamps (timestamp field) are typically in UTC. Store them appropriately (e.g., TIMESTAMPTZ in PostgreSQL) and convert to local timezones only for display purposes.
  • International SMS: Different countries have varying regulations. Some require sender ID registration (India, UAE, Saudi Arabia), while others restrict certain content types. Check Vonage Country-Specific SMS Features before sending internationally.
  • Unknown Status Handling: The unknown status means the carrier didn't provide delivery confirmation (common in certain regions). Treat as uncertain rather than failed. Don't automatically retry, but log for monitoring. If many messages show unknown, consider alternative carriers or channels for that destination.

10. Application Monitoring and Performance Metrics

  • Health Checks: The /health endpoint is a basic start. Production systems often require more detailed checks (e.g., database connectivity).
  • Metrics: Track key metrics: SMS sent count, delivery success rate (delivered / (delivered + failed + expired)), failure counts by error code, webhook processing latency. Use libraries like prom-client to expose metrics for Prometheus/Grafana.

Metrics implementation example:

javascript
const promClient = require('prom-client');

const register = new promClient.Registry();
const smsSentCounter = new promClient.Counter({
    name: 'sms_sent_total',
    help: 'Total SMS messages sent',
    registers: [register]
});
const smsDeliveredCounter = new promClient.Counter({
    name: 'sms_delivered_total',
    help: 'Total SMS messages delivered',
    registers: [register]
});

app.get('/metrics', async (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
});
  • Error Tracking: Integrate with error tracking services like Sentry or Bugsnag to capture and alert on exceptions in both the sending script and the webhook handler.
  • Logging Aggregation: Use a log aggregation platform (ELK stack, Datadog Logs, Grafana Loki) to centralize logs for easier searching and analysis.

11. Vonage Delivery Status Codes and Error Reference

Understanding Vonage's Delivery Receipt (DLR) status codes is essential for proper error handling and monitoring.

Complete DLR Status Codes Reference

Status CodeMeaningDescriptionAction RequiredTypical Time
submittedMessage submittedVonage accepted the message and submitted it to the carrier networkMonitor for final delivery status1–5 seconds
delivered (DELIVRD)Successfully deliveredMessage successfully delivered to recipient's deviceNo action – success case5–30 seconds
failed (FAILED)Delivery failedMessage delivery failed due to technical or carrier issuesCheck error code, consider retry with exponential backoffVaries
rejected (REJECTD)Message rejectedCarrier or platform rejected the message before deliveryCheck error code, do not retry without fixing root cause1–10 seconds
expired (EXPIRED)Message expiredMessage not delivered within validity period (carrier-dependent, typically 24–72 hours)Consider shorter messages or alternative channels24–72 hours
undelivered (UNDELIV)UndeliveredCarrier reports message could not be delivered (device off, out of coverage)May retry after delay or notify userVaries
accepted (ACCEPTD)Accepted by carrierCarrier accepted message but final delivery status pendingWait for final status update5–15 seconds
unknown (UNKNOWN)Status unknownCarrier did not provide delivery confirmationCommon for certain carriers/countries; treat as uncertainN/A
deleted (DELETED)Message deletedMessage was deleted before delivery (rare)Do not retryN/A

Source: Vonage DLR Statuses Documentation (accessed January 2025)

SMS Error Codes and Troubleshooting Guide

When status is failed or rejected, the webhook includes an error object with code and reason fields:

Error CodeDescriptionTypical Cause
1ThrottledRate limit exceeded; implement exponential backoff
2Missing parametersRequired field missing in API request
3Invalid parametersParameter format incorrect (e.g., invalid phone number)
4Invalid credentialsAPI key/secret incorrect or application not authorized
5Internal errorVonage platform issue; retry with exponential backoff
6Invalid messageMessage content violates policies or contains invalid characters
7Number barredRecipient number blocked/barred from receiving messages
8Partner account barredYour Vonage account suspended; contact support
9Partner quota exceededMonthly SMS quota reached; upgrade plan or wait for reset
10Illegal senderSender ID not allowed for destination country
15Illegal senderSender address invalid (same as code 10)
1340Outside allowed windowWhatsApp-specific: message sent outside 24-hour customer care window

Important: Vonage charges for messages regardless of final delivery status, except when rejected by the Vonage platform before submission (certain error codes like 2, 3, 4).

Source: Vonage SMS Delivery Error Codes (accessed January 2025)


12. Common Issues and Debugging Solutions

  • ngrok Issues:
    • Ensure ngrok is running and the correct URL (HTTPS) is configured in the Vonage dashboard Status URL.
    • Free ngrok Account Limitations (2025):
      • Bandwidth: 1 GB per month limit on free tier
      • Interstitial Warning Page: Free tier injects a "Visit Site" interstitial page before all HTML traffic (sets cookie for 7 days after first visit)
      • Random URLs: URLs change on restart unless using paid static domains
      • Single Tunnel: Only 1 simultaneous tunnel allowed on free plan
    • Free ngrok accounts are sufficient for development but consider paid plans for production-like testing or higher bandwidth needs.
    • Firewalls might block ngrok.
  • Vonage Configuration Errors:
    • Incorrect API Credentials/App ID/Private Key: Double-check .env values. Ensure the private key file path (VONAGE_PRIVATE_KEY_PATH) is correct, matches the actual filename, and the file is readable by your application.
    • Messages API Not Default: Verify the account setting for Default SMS Setting.
    • Number Not Linked: Ensure the Vonage number is linked to the correct application.
    • Incorrect Status URL: Verify the URL format (https://…/webhooks/status) in the Vonage application settings.
  • Code Errors:
    • Check server logs (node index.js output) for errors during webhook processing.
    • Check send script logs (node send-sms.js output) for API errors during sending.
    • Ensure dotenv is loading variables correctly (console.log(process.env.VONAGE_API_KEY) early in the script to test).
  • No Webhooks Received:
    • Verify the SMS was actually sent successfully (check send-sms.js log for messageUuid).
    • Check the Vonage dashboard for API logs or error reports related to webhook delivery failures.
    • Confirm ngrok is running and accessible.
    • Wait sufficient time – carrier delivery reports can take time.
  • Delivery Failures (failed/rejected status):
    • Check the error.code and error.reason in the webhook payload.
    • Consult the error codes reference table above. Common reasons include invalid recipient number format, blocked number, carrier restrictions, insufficient account funds.
  • API vs. Delivery: Remember that a successful API response from vonage.messages.send only means Vonage accepted the message for delivery, not that it was delivered. The webhook provides the actual delivery status.

Manual webhook testing with curl:

bash
curl -X POST http://localhost:3000/webhooks/status \
  -H "Content-Type: application/json" \
  -d '{
    "message_uuid": "test-uuid-123",
    "to": "14155559876",
    "from": "14155552671",
    "timestamp": "2025-01-15T18:30:45.123Z",
    "status": "delivered",
    "channel": "sms"
  }'

13. Production Deployment and CI/CD Pipeline Setup

  • Environment Variables: In production, manage .env variables securely using your hosting provider's mechanism (e.g., Heroku Config Vars, AWS Secrets Manager, Docker secrets). Never commit .env files or private keys to Git.
  • Deployment:
    • PaaS (Heroku, Render, etc.): Configure Procfile (web: node index.js), push code, set environment variables via the dashboard/CLI.
    • Docker: Create a Dockerfile to containerize the application. Manage the private.key securely (e.g., mount as a volume or use Docker secrets).
    • Server: Deploy code, install dependencies, manage environment variables, use a process manager like PM2 to keep the server running.
  • CI/CD Pipeline (e.g., GitHub Actions, GitLab CI):
    1. Lint & Format: Check code style.
    2. Test: Run unit and integration tests.
    3. Build (if needed): e.g., Build Docker image.
    4. Deploy: Push code/image to the hosting environment. Manage secrets carefully during deployment.
  • Webhook URL in Production: Use your application's public domain name instead of the ngrok URL in the Vonage Application Status URL settings. Ensure it's HTTPS.
  • Private Key Handling: The private key file needs to be securely transferred to the production environment and referenced by the VONAGE_PRIVATE_KEY_PATH environment variable. Do not store it in version control.

Example Dockerfile:

dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

Example GitHub Actions workflow:

yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test
      - name: Deploy to production
        run: |
          # Add your deployment commands here
        env:
          VONAGE_API_KEY: ${{ secrets.VONAGE_API_KEY }}
          VONAGE_API_SECRET: ${{ secrets.VONAGE_API_SECRET }}

Frequently Asked Questions About Vonage SMS Delivery Status

How do I track SMS delivery status with Vonage?

Configure a webhook endpoint in your Vonage Application settings to receive delivery status callbacks. Vonage sends POST requests with status updates (submitted, delivered, failed, rejected) to your webhook URL containing the message_uuid and status information.

What is the difference between message submitted and delivered status?

submitted means Vonage accepted your message and sent it to the carrier network. delivered confirms the carrier successfully delivered the message to the recipient's device. Always wait for delivered status to confirm actual delivery.

How long does it take to receive delivery status webhooks?

Delivery status webhooks typically arrive within seconds to minutes after sending. However, carrier delays can extend this to several minutes or hours. Some carriers in certain countries may not provide delivery receipts at all.

Why am I not receiving webhooks from Vonage?

Common causes: incorrect webhook URL in Vonage dashboard, ngrok not running, webhook endpoint not responding with 2xx status code, firewall blocking requests, or your SMS was never successfully sent (check send script logs for errors).

How do I verify webhook requests are from Vonage?

Implement JWT signature verification using the signature secret from your Vonage Application settings. Verify the JWT token in the Authorization header, validate the payload_hash claim matches your request body hash, and check the iat timestamp is recent (within 5 minutes).

What HTTP status code should my webhook return?

Always return a 2xx status code (typically 200 OK or 204 No Content) immediately to acknowledge receipt. If you don't respond with 2xx, Vonage assumes delivery failed and retries the webhook, causing duplicate processing.

Can I test webhooks locally without deploying?

Yes, use ngrok to create a public HTTPS tunnel to your local development server. Configure the ngrok URL in your Vonage Application Status URL setting to receive webhooks on your local machine during development.

What does error code 10 mean in delivery status?

Error code 10 ("Illegal sender") means the sender ID or phone number is not allowed for the destination country. Some countries require pre-registered sender IDs or only allow messages from local numbers.

How do I handle duplicate webhook deliveries?

Design your webhook handler to be idempotent. Store processed message_uuid + status combinations and check before processing. Vonage retries webhooks if it doesn't receive a 2xx response, so duplicates can occur during network issues.

Does Vonage charge for failed messages?

Yes, Vonage charges for messages regardless of final delivery status, except when rejected by the Vonage platform before submission (error codes 2, 3, 4). Carrier rejections and delivery failures still incur charges.

Frequently Asked Questions

How to send SMS messages with Node.js and Vonage

Use the Vonage Messages API with the @vonage/server-sdk in your Node.js application. The sendSms function demonstrates how to send text messages by specifying the recipient, sender, and message content. Remember to set up your API key, secret, application ID, private key path, and phone numbers correctly in your .env file.

What is the Vonage Messages API?

The Vonage Messages API is a unified interface for sending various types of messages, including SMS. It offers robust features and multi-channel capabilities, making it suitable for applications needing reliable messaging and status tracking.

Why does Vonage use webhooks for status updates?

Webhooks provide real-time delivery status updates, including 'delivered,' 'failed,' or 'rejected,' directly to your application. This approach eliminates the need for constant polling and allows for immediate responses to message statuses, enabling better reliability and logging.

When should I use ngrok for Vonage webhooks?

ngrok is essential during local development to expose your local server and receive webhooks from Vonage. For production deployments, you must use your server's public HTTPS URL configured in your Vonage application settings.

Can I track SMS delivery status with Vonage?

Yes, by setting up a webhook endpoint (e.g., /webhooks/status) in your Node.js application. Vonage will send real-time status updates to this endpoint, which you can then process to track deliveries, failures, and other events.

How to set up Vonage Messages API with Node.js

First, install the @vonage/server-sdk package. Then, configure your Vonage account, create an application, and obtain the necessary credentials (API Key, API Secret, Application ID, and Private Key). Finally, initialize the Vonage client in your Node.js code using these credentials.

What is the purpose of the private key in Vonage?

The private key, along with the application ID, is crucial for authenticating your Node.js application with the Vonage Messages API and is the standard method when an application context is needed. This ensures secure communication and prevents unauthorized access to your account.

Why is a 200 OK response important for Vonage webhooks?

Responding with a 2xx status code (like 200 OK) is mandatory to acknowledge successful receipt of the Vonage webhook. Failure to respond correctly will cause Vonage to retry the webhook, leading to potential duplicate processing.

How to handle Vonage webhook retries in Node.js

Design your webhook handler to be idempotent using the message_uuid, ensuring it can process the same status update multiple times without causing issues. If processing is lengthy, respond with 200 OK immediately and process asynchronously.

What are common Vonage SMS delivery failure reasons?

Check the error code and reason in the webhook payload. Common reasons include an invalid recipient number, the recipient blocking the number, carrier-specific restrictions, or insufficient funds in your Vonage account.

What data should I store for SMS tracking?

Store the message UUID, sender and recipient numbers, content, timestamps, status updates, and any error codes. Consider the provided conceptual database schema as a starting point.

How to verify Vonage webhook signatures

Consult the Vonage Messages API documentation. They might use HMAC-SHA256 with your API secret or a dedicated webhook signing secret. Check if the Vonage SDK offers helper functions for signature verification.

How to handle Vonage message concatenation?

Long SMS are split, but share a message UUID. Status updates may be per part or as a whole. Your logic should accommodate this, potentially grouping status updates by message UUID.

What security measures to consider when using Vonage webhooks

Implement webhook signature verification to prevent unauthorized requests. Use HTTPS, IP whitelisting if possible, and input sanitization to minimize security risks.

When does Vonage retry webhooks?

Vonage retries webhook delivery if your endpoint doesn't return a 2xx HTTP status code within a short time frame, indicating that the message hasn't been processed successfully.