code examples

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

Vonage SMS Delivery Receipts with Node.js & Express: Complete Webhook Integration Guide

Build a production-ready Node.js application with Express to send SMS via Vonage API and track delivery status through webhooks. Includes validation, error handling, and real-time DLR processing.

This guide provides a step-by-step walkthrough for building a Node.js application using Express to send SMS messages via the Vonage SMS API and receive real-time delivery status updates through webhooks.

By the end of this tutorial, you'll have built a functional application capable of:

  1. Sending SMS messages programmatically.
  2. Receiving and processing delivery status updates (Delivery Receipts or DLRs) from Vonage.
  3. Handling configuration, security, and error logging.

You'll solve the common need for applications to confirm whether SMS notifications were successfully delivered to the recipient's handset – crucial for reliable communication workflows.1

Technologies Used

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express.js: A minimal and flexible Node.js web application framework for creating API endpoints and handling webhooks.
  • Vonage SMS API: Enables sending and receiving SMS messages globally. You'll use the @vonage/server-sdk Node.js library.
  • dotenv: A module to load environment variables from a .env file into process.env.
  • ngrok: A tool to expose local development servers to the internet, necessary for testing webhooks.
  • nodemon (Optional): A utility that monitors changes in your source and automatically restarts your server during development.
  • express-validator: Middleware for request validation.

System Architecture

The basic flow of your application works as follows:

  1. Sending: An API client (e.g., Postman, another service) makes a POST request to your application's /send-sms endpoint.
  2. Your Express application receives the request, validates it, and uses the Vonage SDK to send the SMS message via the Vonage API.
  3. Vonage delivers the SMS to the recipient's carrier network.
  4. Receiving Delivery Receipt: Once the final status of the message is known (e.g., delivered, failed, expired), Vonage sends a POST request (webhook) containing the delivery status information to a predefined endpoint in your application (/delivery-receipt).
  5. Your Express application receives the webhook, parses the status, and logs it (in a real-world scenario, you'd likely update a database record).
+-------------+ +------------------------+ +----------------+ +-----------------+ | API Client |------>| Your Node.js/Express App |------>| Vonage API |------>| Recipient Phone | | (Postman) | (1) | (localhost:3000) | (2) | (sms.nexmo.com)| (3) | | +-------------+ +------------------------+ +----------------+ +-----------------+ ^ | | | (5) Webhook POST /delivery-receipt | (4) Delivery Status | +--------------------------------------+----------------------+ (via ngrok tunnel)

Prerequisites

  • Node.js and npm (or yarn): Installed on your system. (Download Node.js)
  • Vonage API Account: Sign up for free at Vonage API Dashboard.
  • Vonage API Key and Secret: Found on the main page of your Vonage Dashboard after signing in.
  • Vonage Virtual Number: You need a Vonage phone number capable of sending SMS. You can purchase one from the dashboard: Numbers > Buy Numbers.
  • ngrok: Installed and authenticated. (Download ngrok)
  • Basic understanding of JavaScript, Node.js, Express, and REST APIs.

1. Setting Up the Project

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

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

    bash
    mkdir vonage-sms-dlr-guide
    cd vonage-sms-dlr-guide
  2. Initialize Node.js Project: Initialize the project using npm, accepting the defaults.

    bash
    npm init -y

    This creates a package.json file.

  3. Enable ES Modules: Since you'll use ES Module import syntax (e.g., import express from 'express'), tell Node.js to treat .js files as modules. Open your package.json file and add the line "type": "module" at the top level:

    json
    // package.json
    {
      "name": "vonage-sms-dlr-guide",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "type": "module", // Add this line
      "scripts": {
        "start": "node index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
      // Dependencies will be added below
    }
  4. Install Dependencies: Install Express for the web server, the Vonage Server SDK for interacting with the API, dotenv for managing environment variables, express-validator for input validation, and optionally nodemon for development convenience.

    bash
    npm install express @vonage/server-sdk dotenv express-validator
    npm install --save-dev nodemon # Optional: for development
  5. Configure nodemon (Optional): If you installed nodemon, add a development script to your package.json. Open package.json and add/modify the scripts section:

    json
    // package.json snippet (inside the main JSON object)
      "scripts": {
        "start": "node index.js",
        "dev": "nodemon index.js", // Add this line
        "test": "echo \"Error: no test specified\" && exit 1"
      },

    This allows you to run npm run dev to start your server with auto-reloading on file changes.

  6. Create Core Files: Create the main application file and files for environment variables and ignored files.

    bash
    touch index.js .env .gitignore
  7. Configure .gitignore: Prevent sensitive information and unnecessary files from being committed to version control. Add the following to your .gitignore file:

    text
    # .gitignore
    node_modules
    .env
    npm-debug.log
    *.log
  8. Configure .env: Add placeholders for your Vonage credentials and number. You'll also add a BASE_URL which will be your ngrok URL later. Don't commit this file to Git.

    dotenv
    # .env
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
    PORT=3000
    BASE_URL=http://localhost:3000 # Replace with ngrok URL during testing
    • VONAGE_API_KEY: Your API key from the Vonage Dashboard.
    • VONAGE_API_SECRET: Your API secret from the Vonage Dashboard.
    • VONAGE_NUMBER: The Vonage virtual number you purchased (in E.164 format, e.g., +14155550100).
    • PORT: The port your Express server will listen on.
    • BASE_URL: The public-facing base URL of your application. Crucial for webhook configuration.

2. Implementing Core Functionality: Sending SMS

Write the code to initialize Express and the Vonage SDK, and create an endpoint to send an SMS message.

javascript
// index.js
import express from 'express';
import dotenv from 'dotenv';
import { Vonage } from '@vonage/server-sdk';

// Load environment variables
dotenv.config();

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

// --- Vonage Setup ---
// Validate essential environment variables
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_NUMBER) {
    console.error('Error: Missing Vonage API credentials or number in .env file.');
    process.exit(1); // Exit if critical configuration is missing
}

const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY,
    apiSecret: process.env.VONAGE_API_SECRET,
    // Optional: Set a custom user agent for your requests
    // userAgent: "my-vonage-app/1.0.0"
});

// --- API Endpoints ---

// Endpoint to send an SMS
app.post('/send-sms', async (req, res) => {
    const { to, text } = req.body;

    // Basic validation (More robust validation will be added later)
    if (!to || !text) {
        return res.status(400).json({ error: 'Missing "to" or "text" field in request body.' });
    }
    // Very basic check – E.164 format (like +14155550100) is recommended and better validated later
    if (!/^\+?\d{10,15}$/.test(to)) {
         return res.status(400).json({ error: '"to" field must be a valid phone number, preferably in E.164 format (e.g., +14155550100).' });
    }

    console.log(`Attempting to send SMS to: ${to}`);

    try {
        const resp = await vonage.sms.send({
            to: to,
            from: process.env.VONAGE_NUMBER,
            text: text,
            // Optional: Specify 'unicode' for messages with non-GSM characters (like emojis)
            // type: 'unicode'
        });

        console.log('Message submission response:', resp);
        // Note: `resp` contains information about the *submission* to Vonage,
        // not the final delivery status. The `message-id` is key here.
        if (resp.messages[0].status === '0') {
            res.status(200).json({
                message: 'SMS submitted successfully!',
                messageId: resp.messages[0]['message-id']
            });
        } else {
            // Handle submission errors reported directly by the SDK response
            console.error(`Message submission failed with error: ${resp.messages[0]['error-text']}`);
            res.status(400).json({ // Use 400 for submission errors like invalid number format reported here
                error: `Message submission failed: ${resp.messages[0]['error-text']}`,
                errorCode: resp.messages[0].status
            });
        }

    } catch (error) {
        console.error('Error sending SMS via Vonage API:', error);
        // Provide more context if available from the Vonage error object
        let errorMessage = 'Failed to send SMS due to a server or network error.';
        if (error.response && error.response.data) {
             console.error('Vonage API Error Details:', error.response.data);
             errorMessage = error.response.data.title || error.response.data.detail || errorMessage;
             // Handle specific known error codes if needed
             // if (error.response.data.status === '401') errorMessage = 'Authentication failed. Check API key/secret.';
        }
        res.status(500).json({ error: errorMessage, details: error.message });
    }
});

// --- Start Server ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server listening on http://localhost:${port}`);
    // Remind user about ngrok if BASE_URL is still localhost
    if (process.env.BASE_URL && process.env.BASE_URL.includes('localhost')) {
        console.warn('Reminder: Your BASE_URL in .env is set to localhost. You\'ll need to update it with your ngrok URL and configure Vonage webhooks for delivery receipts to work.');
    }
});

// --- Graceful Shutdown (Optional but recommended) ---
process.on('SIGINT', () => {
  console.log('Received SIGINT. Shutting down gracefully…');
  // Add any cleanup logic here (e.g., close database connections)
  process.exit(0);
});

Explanation:

  1. Imports: Import express, dotenv, and Vonage from @vonage/server-sdk.
  2. Environment Variables: dotenv.config() loads variables from .env. Add a check to ensure critical Vonage variables are present.
  3. Express Setup: Initialize Express and use middleware (express.json, express.urlencoded) to easily parse incoming request bodies.
  4. Vonage Initialization: Create a Vonage client instance using the API key and secret from your environment variables.
  5. /send-sms Endpoint:
    • Defines a POST route at /send-sms.
    • Extracts the recipient number (to) and message content (text) from the request body.
    • Performs basic validation (improved later) to ensure to and text are provided and to looks somewhat like a phone number (clarifying E.164 format is preferred).
    • Uses vonage.sms.send() within an async function to send the message. This method requires to, from (your Vonage number), and text.
    • Important: The response from vonage.sms.send() confirms submission to Vonage, not final delivery. It includes a status code ('0' means success) and the message-id. Check this status for immediate feedback.
    • Includes try...catch for handling network or API-level errors during the call. It logs errors and returns appropriate status codes (400 for submission errors, 500 for server errors).
  6. Server Start: The server starts listening on the port defined in .env (or default 3000). A warning reminds you about ngrok if the BASE_URL hasn't been updated.
  7. Graceful Shutdown: Handles SIGINT (Ctrl+C) for a cleaner shutdown process.

3. Handling Delivery Status Callbacks (Webhooks)

Vonage uses webhooks to send Delivery Receipts (DLRs) back to your application. We need an endpoint to receive these POST requests.

  1. Set up ngrok: Open a new terminal window and run ngrok to expose your local server running on port 3000.

    bash
    ngrok http 3000

    ngrok will display forwarding URLs (e.g., https://<random-string>.ngrok-free.app). Copy the https URL. This is your public URL.

  2. Update .env: Replace the BASE_URL in your .env file with the https URL provided by ngrok.

    dotenv
    # .env
    # ... other variables ...
    BASE_URL=https://<random-string>.ngrok-free.app

    Restart your Node.js application (npm run dev or npm start) for the changes in .env to take effect.

  3. Configure Vonage Webhooks:

    • Go to your Vonage API Dashboard.
    • Navigate to Settings in the left-hand menu.
    • Find the API settings section (you might need to scroll down).
    • In the SMS Settings card, ensure "SMS API" is selected.
    • In the Delivery receipts (DLR) field under "Webhook URL for DLR", enter your full ngrok webhook URL: YOUR_NGROK_HTTPS_URL/delivery-receipt (e.g., https://<random-string>.ngrok-free.app/delivery-receipt).
    • Set the HTTP Method to POST.
    • Click Save changes.
    • Note: This guide focuses on the SMS API's specific DLR format. Vonage also offers a newer Messages API which uses a different endpoint (/webhooks/dlr) and provides a unified webhook format for multiple channels (SMS, WhatsApp, etc.). For new implementations, you might consider using the Messages API instead, although its setup is slightly different.
  4. Implement the Webhook Endpoint: Add the following route handler to your index.js file, typically before the app.listen call.

    javascript
    // index.js
    
    // ... (after the /send-sms endpoint) ...
    
    // Endpoint to receive Delivery Receipts (DLRs) from Vonage
    app.post('/delivery-receipt', (req, res) => {
        // Vonage might send DLRs as application/json or application/x-www-form-urlencoded
        // Express middleware handles parsing both based on Content-Type
        const params = req.body;
    
        // Log the raw DLR for debugging
        console.log('--- Delivery Receipt Received ---');
        console.log(JSON.stringify(params, null, 2)); // Pretty print the JSON or parsed form data
    
        // Basic validation: Check for essential DLR fields (names vary slightly based on format)
        // Common fields: messageId (or message-id), msisdn (recipient), status, err-code, price, scts (timestamp)
        const messageId = params['message-id'] || params.messageId;
        const status = params.status;
        const recipient = params.msisdn;
    
        if (!messageId || !status || !recipient) {
            console.warn('Received incomplete DLR:', params);
            // Respond with 200 OK even if incomplete, Vonage expects this.
            return res.status(200).send('OK');
        }
    
        const errorCode = params['err-code'] || params.errCode; // Error code if status is 'failed' or 'rejected'
        const price = params.price; // Cost of the message segment
        const timestamp = params.scts; // Timestamp of the status update
        const networkCode = params['network-code'] || params.networkCode; // Network code
    
        console.log(`DLR Info: Recipient=${recipient}, Message ID=${messageId}, Status=${status}, ErrorCode=${errorCode || 'N/A'}, Timestamp=${timestamp}`);
    
        // --- TODO: Implement your business logic here ---
        // Examples:
        // - Find the message in your database using the messageId.
        // - Update its status to the received 'status'.
        // - Store the errorCode, timestamp, price, etc.
        // - Trigger follow-up actions based on the status (e.g., retry failed messages, notify internally).
        // - Log metrics for deliverability analysis.
    
        // Example: Log specific statuses
        if (status === 'delivered') {
            console.log(`Message ${messageId} successfully delivered to ${recipient}.`);
        } else if (status === 'failed' || status === 'rejected') {
            console.error(`Message ${messageId} to ${recipient} failed with status: ${status}, error code: ${errorCode}. Check Vonage docs for details.`);
            // You might want to query Vonage documentation or map codes to reasons here.
        } else {
             console.log(`Message ${messageId} to ${recipient} has status: ${status}.`);
        }
    
        // IMPORTANT: Always respond to Vonage webhooks with a 200 OK status.
        // Failure to do so may result in Vonage retrying the webhook, leading to duplicate processing.
        res.status(200).send('OK');
    });
    
    // ... (app.listen call) ...

Explanation:

  1. Route Definition: We define a POST route at /delivery-receipt, matching the URL configured in the Vonage dashboard.
  2. Parsing: We access the incoming webhook data from req.body. Express's json() and urlencoded() middleware handle parsing based on the Content-Type header sent by Vonage.
  3. Logging: The raw DLR payload is logged for inspection. Key fields like message-id, status, err-code, msisdn (recipient), and scts (timestamp) are extracted and logged. Note that field names might vary slightly (e.g., message-id vs messageId), so we check for both common variants.
  4. Business Logic Placeholder: A TODO comment highlights where you would integrate this data into your application's logic (e.g., database updates).
  5. Status Handling: Basic examples show how to check for specific statuses like delivered or failed.
  6. Crucial Response: We always send a 200 OK response back to Vonage using res.status(200).send('OK'). This acknowledges receipt of the webhook. If Vonage doesn't receive a 200 OK, it will assume the webhook failed and may retry sending it.

4. Building a Complete API Layer

Let's refine the /send-sms endpoint with robust validation using express-validator.

  1. Update /send-sms with Validation: Modify the /send-sms endpoint definition in index.js.

    javascript
    // index.js
    import express from 'express';
    import dotenv from 'dotenv';
    import { Vonage } from '@vonage/server-sdk';
    import { body, validationResult } from 'express-validator'; // Import validator
    
    // ... (dotenv.config, express app setup, Vonage init) ...
    
    // Validation rules for /send-sms
    const sendSmsValidationRules = [
        body('to')
            .notEmpty().withMessage('Recipient phone number (""to"") is required.')
            // isMobilePhone is generally good, but E.164 is the most reliable format.
            // Provide flexibility but guide towards E.164.
            .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format for ""to"" field. Recommended format is E.164 (e.g., +14155550100).'),
        body('text')
            .notEmpty().withMessage('Message text (""text"") is required.')
            .isLength({ min: 1, max: 1600 }).withMessage('Message text must be between 1 and 1600 characters.'), // Example length constraints
    ];
    
    // Middleware to handle validation results
    const validateRequest = (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            // Log validation errors for debugging
            console.warn('Validation Errors:', errors.array());
            return res.status(400).json({ errors: errors.array() });
        }
        next();
    };
    
    // Endpoint to send an SMS (Updated with validation)
    app.post('/send-sms', sendSmsValidationRules, validateRequest, async (req, res) => {
        // Validation passed if we reach here
        const { to, text } = req.body;
    
        console.log(`Attempting to send SMS to: ${to}`);
    
        try {
            const resp = await vonage.sms.send({
                to: to,
                from: process.env.VONAGE_NUMBER,
                text: text,
            });
    
            console.log('Message submission response:', resp);
            if (resp.messages[0].status === '0') {
                res.status(200).json({
                    message: 'SMS submitted successfully!',
                    messageId: resp.messages[0]['message-id']
                });
            } else {
                console.error(`Message submission failed with error: ${resp.messages[0]['error-text']}`);
                res.status(400).json({ // Use 400 for submission errors
                    error: `Message submission failed: ${resp.messages[0]['error-text']}`,
                    errorCode: resp.messages[0].status
                });
            }
        } catch (error) {
            console.error('Error sending SMS via Vonage API:', error);
            let errorMessage = 'Failed to send SMS due to a server or network error.';
            if (error.response && error.response.data) {
                 console.error('Vonage API Error Details:', error.response.data);
                 errorMessage = error.response.data.title || error.response.data.detail || errorMessage;
            }
            res.status(500).json({ error: errorMessage, details: error.message });
        }
    });
    
    // ... (delivery-receipt endpoint, app.listen) ...

    Changes:

    • Imported body and validationResult from express-validator.
    • Defined sendSmsValidationRules array specifying rules for to and text. We use isMobilePhone for general validation but message guides towards E.164. Added length constraints for text.
    • Created a validateRequest middleware function to check validationResult and return errors if any.
    • Added the rules array and the validation middleware to the app.post('/send-sms', ...) definition before the main async handler.
    • Removed the initial, basic validation from the main handler as express-validator now handles it more robustly.
    • Error handling logic remains similar, checking the direct response status from vonage.sms.send and catching broader API/network errors.
  2. API Testing Examples (curl):

    • Send SMS (Success): Replace placeholders with your actual recipient number (use E.164 format like +14155550100).

      bash
      curl -X POST http://localhost:3000/send-sms \
      -H ""Content-Type: application/json"" \
      -d '{
        ""to"": ""+14155550100"",
        ""text"": ""Hello from Vonage and Node.js! This is a test with validation.""
      }'

      Expected Response (200 OK):

      json
      {
        ""message"": ""SMS submitted successfully!"",
        ""messageId"": ""some-message-id-from-vonage""
      }
    • Send SMS (Validation Error - Missing 'text'):

      bash
      curl -X POST http://localhost:3000/send-sms \
      -H ""Content-Type: application/json"" \
      -d '{
        ""to"": ""+14155550100""
      }'

      Expected Response (400 Bad Request):

      json
      {
        ""errors"": [
          {
            ""type"": ""field"",
            ""msg"": ""Message text (\""text\"") is required."",
            ""path"": ""text"",
            ""location"": ""body""
          },
          {
             ""type"": ""field"",
             ""value"": """",
             ""msg"": ""Message text must be between 1 and 1600 characters."",
             ""path"": ""text"",
             ""location"": ""body""
          }
        ]
      }
    • Send SMS (Validation Error - Invalid 'to'):

      bash
       curl -X POST http://localhost:3000/send-sms \
       -H ""Content-Type: application/json"" \
       -d '{
         ""to"": ""invalid-number"",
         ""text"": ""Testing invalid number""
       }'

      Expected Response (400 Bad Request):

      json
      {
        ""errors"": [
          {
            ""type"": ""field"",
            ""value"": ""invalid-number"",
            ""msg"": ""Invalid phone number format for \""to\"" field. Recommended format is E.164 (e.g., +14155550100)."",
            ""path"": ""to"",
            ""location"": ""body""
          }
        ]
      }
    • Delivery Receipt Webhook (Simulated): You can't easily simulate the exact Vonage DLR POST with curl without knowing a valid message-id beforehand, but you can test if your endpoint receives a POST correctly. Use the ngrok inspector (http://127.0.0.1:4040 in your browser) to see the real DLRs coming from Vonage after sending a message.


5. Integrating with Vonage Services (Recap & Details)

Let's consolidate the key integration points with Vonage.

  • API Credentials:

    • How to Obtain: Log in to the Vonage API Dashboard. Your API Key and API Secret are displayed prominently on the main page ("API key" and "Secret").
    • Secure Storage: Store these values in your .env file (VONAGE_API_KEY, VONAGE_API_SECRET) and ensure .env is listed in your .gitignore. Never hardcode credentials directly in your source code.
    • Purpose: Used by the @vonage/server-sdk to authenticate your API requests.
  • Virtual Number:

    • How to Obtain: In the dashboard, navigate to Numbers > Buy Numbers. Search for numbers by country and features (ensure SMS is selected). Purchase a number. Alternatively, use the Vonage CLI (vonage numbers:search GB --features=SMS, vonage numbers:buy <number> GB).
    • Secure Storage: Store the purchased number (in E.164 format, e.g., +14155550100) in your .env file (VONAGE_NUMBER).
    • Purpose: Acts as the 'sender ID' for your outgoing SMS messages in many regions (required in the US/Canada).
  • Webhook Configuration (Delivery Receipts):

    • How to Configure: Dashboard > Settings > API settings > SMS Settings card.
    • URL: Set the "Delivery receipts (DLR)" URL to YOUR_NGROK_HTTPS_URL/delivery-receipt. Must be publicly accessible (hence ngrok).
    • Method: Set to POST.
    • Purpose: Tells Vonage where to send the delivery status updates for messages sent using your API key via the SMS API.
    • Environment Variable (BASE_URL): Storing the ngrok URL (or your production URL) in .env (BASE_URL) helps manage this, although it's primarily used here for the reminder log message. The critical part is pasting the correct URL into the Vonage dashboard.

6. Error Handling, Logging, and Retry Mechanisms

  • Error Handling Strategy:

    • API Calls (/send-sms): Use try...catch blocks around vonage.sms.send(). Check the direct response status (resp.messages[0].status). Log detailed errors (console.error). Check error.response.data for specific Vonage API error details if available during exceptions. Return appropriate HTTP status codes (400 for validation/submission errors, 500 for server/API errors) with clear JSON error messages.
    • Webhooks (/delivery-receipt): Log incoming DLRs. Use try...catch around your internal processing logic (e.g., database updates). Crucially, always return 200 OK to Vonage, even if your internal processing fails, to prevent retries. Log internal processing errors separately.
  • Logging:

    • Use console.log for informational messages (sending attempts, DLR received, status updates).
    • Use console.warn for non-critical issues (e.g., validation errors, incomplete DLRs).
    • Use console.error for actual errors (API call failures, submission failures, internal processing failures).
    • Production: Replace console.* with a structured logger like Pino or Winston. This allows for log levels, JSON formatting, and easier integration with log management systems.
      bash
      # Example using Pino (Install: npm install pino)
      # import pino from 'pino';
      # const logger = pino();
      # logger.info({ messageId: '...', status: 'submitted' }, 'SMS submitted');
      # logger.error({ err: error, messageId: '...' }, 'Failed to send SMS');
  • Retry Mechanisms (Vonage):

    • Sending: If vonage.sms.send() fails due to a temporary network issue or a 5xx error from Vonage (caught in the catch block), you might implement application-level retries (e.g., using a library like async-retry) with exponential backoff. However, be cautious not to retry on permanent errors (like invalid number format - 4xx errors or submission errors indicated in the response).
    • Receiving DLRs: Vonage handles retries automatically if your webhook endpoint doesn't respond with 200 OK within a reasonable timeout (usually several seconds). Your primary responsibility is to ensure your /delivery-receipt endpoint is reliable and responds quickly with 200 OK. Offload heavy processing (like database writes) to happen asynchronously after sending the 200 OK, perhaps using a message queue.
  • Common Vonage DLR Statuses/Errors:

    • delivered: Successfully delivered to the handset.
    • accepted: Accepted by the carrier, awaiting final status.
    • failed: Delivery failed (e.g., number invalid, blocked). Check err-code.
    • expired: Message validity period expired before delivery.
    • rejected: Rejected by Vonage or carrier (e.g., spam filter, permission issue). Check err-code.
    • buffered: Temporarily stored, awaiting delivery attempt.
    • Refer to the Vonage SMS API Error Codes Documentation for detailed err-code meanings.

7. Database Schema and Data Layer (Conceptual)

While this guide doesn't implement a database, in a production system, you'd need one to track message status.

  • Why: To persistently store the messageId received when sending and associate it with the subsequent delivery status received via webhook. This allows you to query the status of any message sent.

  • Example Schema (Conceptual - e.g., PostgreSQL):

    sql
    CREATE TABLE sms_messages (
        id SERIAL PRIMARY KEY,
        vonage_message_id VARCHAR(50) UNIQUE NOT NULL, -- From send response & DLR
        recipient_number VARCHAR(20) NOT NULL,
        sender_number VARCHAR(20) NOT NULL,         -- Your Vonage number used
        message_text TEXT,
        status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, expired, rejected, accepted
        vonage_status_code VARCHAR(10),          -- DLR err-code
        price DECIMAL(10, 5),                    -- DLR price
        network_code VARCHAR(10),                -- DLR network-code
        submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
        last_updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Updated on DLR receipt
        dlr_timestamp TIMESTAMP WITH TIME ZONE       -- DLR scts field
    );
    
    -- Optional: Index for querying by message ID or status
    CREATE INDEX idx_sms_vonage_message_id ON sms_messages (vonage_message_id);
    CREATE INDEX idx_sms_status ON sms_messages (status);
  • Data Layer Logic:

    1. On Send (/send-sms success): Insert a new record into sms_messages with the vonage_message_id, recipient, sender, text, and set status to 'submitted'.
    2. On Receive DLR (/delivery-receipt):
      • Find the record in sms_messages using the vonage_message_id from the webhook payload.
      • If found, update the status, vonage_status_code, price, network_code, dlr_timestamp, and last_updated_at fields based on the DLR data.
      • Handle cases where the messageId might not be found (log an error).
      • Implement logic based on the updated status (e.g., trigger notifications for failures).

Conclusion

You have successfully built a Node.js application using Express that can send SMS messages via the Vonage API and receive delivery status updates through webhooks. You've learned how to:

  • Set up a Node.js project with necessary dependencies.
  • Initialize the Vonage Server SDK.
  • Create an API endpoint (/send-sms) to send messages, including input validation.
  • Configure ngrok to expose your local server for webhook testing.
  • Set up webhook URLs in the Vonage Dashboard.
  • Implement a webhook endpoint (/delivery-receipt) to receive and process DLRs.
  • Understand basic error handling, logging, and the importance of responding correctly to webhooks.
  • Conceptualize how to integrate message tracking with a database.

This foundation allows you to build more complex communication workflows requiring reliable SMS delivery confirmation. Remember to replace placeholder credentials and URLs with your actual values and consider using a structured logger and robust database integration for production environments.


References

Footnotes

  1. Vonage Developer Documentation. "SMS Delivery Receipts API Guide." Vonage API Documentation. Accessed October 2025. https://developer.vonage.com/en/messaging/sms/guides/delivery-receipts. Explains how delivery receipts work, DLR status messages, and error codes for tracking SMS delivery confirmation.