code examples

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

Implementing SMS Delivery Status Webhooks in Node.js with Vonage

A step-by-step guide to building a Node.js Express webhook endpoint for receiving and processing real-time Vonage SMS Delivery Receipts (DLRs).

Implementing SMS Delivery Status Webhooks in Node.js with Vonage

Reliably sending SMS messages is crucial, but knowing if and when they arrive is equally important for many applications. Simply getting a successful API response when sending doesn't guarantee delivery to the end user's handset. Factors like network issues, invalid numbers, or carrier filtering can prevent successful delivery.

This guide provides a step-by-step walkthrough for building a robust webhook endpoint using Node.js and Express to receive real-time SMS Delivery Receipts (DLRs) from Vonage. This enables your application to track message status accurately, react to delivery failures, and provide better feedback to users or internal systems.

Project Overview and Goals

Goal: To create a Node.js Express application featuring a webhook endpoint that listens for, receives, processes, and logs SMS Delivery Receipts (DLRs) sent by the Vonage platform.

Problem Solved: This implementation provides near real-time visibility into the final delivery status of SMS messages sent via the Vonage API, moving beyond the initial ""message accepted by Vonage"" confirmation. This is essential for:

  • Confirming successful message delivery to end-users.
  • Identifying and reacting to failed deliveries (e.g., retrying, notifying support, updating user status).
  • Building accurate reporting and analytics on SMS campaign effectiveness.
  • Troubleshooting delivery issues with specific carriers or numbers.

Technologies Involved:

  • Node.js: A JavaScript runtime environment ideal for building scalable, event-driven network applications like webhooks.
  • Express.js: A minimal and flexible Node.js web application framework used to quickly set up the web server and API endpoint for the webhook.
  • Vonage SMS API: The Vonage service used to send SMS messages and configure the webhook URL for receiving DLRs.
  • ngrok (for development): A tool to expose local development servers to the internet, allowing Vonage to send webhook requests to your machine during testing.
  • dotenv: A module to load environment variables from a .env file into process.env, keeping sensitive credentials out of source code.

System Architecture:

+-------------+ +-------------------+ +-----------------+ +--------------+ +-----------------+ | Your |----->| Vonage Send SMS |----->| Carrier Network |----->| User's Phone | | Vonage Platform | | Application | | API Endpoint | | | | | | (DLR Service) | +-------------+ +-------------------+ +-----------------+ +--------------+ +--------+--------+ ^ | | (Delivery Status Update) | (Webhook POST) | | +------------------------------------------------------------------------------------------------+ | +---------------------+ +----------------------------------| Your Node.js/Express| | Webhook Endpoint | | (Receives DLR) | +---------------------+

Prerequisites:

  • Node.js and npm (or yarn): Installed on your development machine. Download from nodejs.org.
  • Vonage API Account: Sign up for free at Vonage API Dashboard. You get free credit to start.
  • Vonage API Key and Secret: Found at the top of the Vonage API Dashboard after signing in.
  • Vonage Virtual Phone Number: Purchase one via the Dashboard (Numbers > Buy Numbers). Ensure it's SMS-capable in your target region.
  • ngrok: Installed and authenticated (a free account is sufficient). Download from ngrok.com.

1. Setting up the Project

Let's initialize our Node.js project and install the necessary dependencies.

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

    bash
    mkdir vonage-sms-dlr-webhook
    cd vonage-sms-dlr-webhook
  2. Initialize Node.js Project: This creates a package.json file to manage project dependencies and scripts.

    bash
    npm init -y
  3. Install Dependencies: We need express for the web server, and dotenv to manage environment variables securely.

    bash
    npm install express dotenv
    • express: The web framework.
    • dotenv: Loads environment variables from a .env file.
  4. Create Core Files: Create the main application file and files for environment variables and Git ignore rules.

    bash
    touch index.js .env .gitignore
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing dependencies and sensitive credentials to version control.

    text
    # .gitignore
    
    node_modules/
    .env
  6. Set Up Environment Variables (.env): Open the .env file and add placeholders for your Vonage credentials and the port your server will run on.

    dotenv
    # .env
    
    # Vonage API Credentials
    # Found on your Vonage Dashboard: https://dashboard.vonage.com/settings
    VONAGE_API_KEY=YOUR_API_KEY
    VONAGE_API_SECRET=YOUR_API_SECRET
    
    # Server Configuration
    PORT=3000 # The port your local server will listen on
    • VONAGE_API_KEY / VONAGE_API_SECRET: Obtain these from your Vonage Dashboard. They authenticate your application with Vonage APIs (though not strictly needed for receiving this simple webhook, they are good practice to have configured).
    • PORT: The local port your Express server will listen on. We'll use 3000 in this guide.
  7. Project Structure: Your basic project structure should now look like this:

    vonage-sms-dlr-webhook/ ├── .env ├── .gitignore ├── index.js ├── node_modules/ ├── package.json └── package-lock.json

    This structure separates configuration (.env) from code (index.js) and keeps sensitive data out of Git.

2. Configuring Vonage and Ngrok

Before writing the webhook code, we need to tell Vonage where to send the delivery receipts. Since our application will run locally during development, we use ngrok to create a temporary public URL that tunnels requests to our local machine.

  1. Retrieve Vonage Credentials:

    • Log in to your Vonage API Dashboard.
    • Your API Key and API Secret are displayed prominently near the top.
    • Copy these values and paste them into your .env file, replacing YOUR_API_KEY and YOUR_API_SECRET.
  2. Ensure You Have a Vonage Number:

    • Navigate to ""Numbers"" > ""Your numbers"" in the dashboard.
    • If you don't have one, go to ""Numbers"" > ""Buy numbers"", search for a number with SMS capability in your desired country, and purchase it. You'll need this number to send messages from, which will trigger the DLRs.
  3. Start ngrok: Open a new terminal window (keep your project terminal open) and start ngrok, telling it to forward HTTP traffic to the port defined in your .env file (e.g., 3000).

    bash
    ngrok http 3000

    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://xxxxxxxxxxxx.ngrok.io -> http://localhost:3000 Forwarding https://xxxxxxxxxxxx.ngrok.io -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
    • Important: Copy the https:// Forwarding URL (e.g., https://xxxxxxxxxxxx.ngrok.io). This is your temporary public URL. Keep this ngrok terminal window running. If you stop and restart ngrok, you will get a new URL and must update the Vonage configuration.
  4. Configure Vonage DLR Webhook URL:

    • Go back to your Vonage API Dashboard.
    • Navigate to ""Settings"" from the left-hand menu.
    • Scroll down to the ""SMS settings"" section.
    • Find the field labeled ""Delivery receipts (DLR) webhooks"".
    • Paste your ngrok https:// Forwarding URL into the field.
    • Append a specific path for your webhook endpoint to the URL. Let's use /webhooks/delivery-receipts. The full URL will look like: https://xxxxxxxxxxxx.ngrok.io/webhooks/delivery-receipts
    • Ensure the HTTP Method dropdown next to it is set to POST. Vonage sends DLR data in the request body via POST.
    • Click the ""Save changes"" button.

    The ""SMS settings"" section contains fields for Default SMS Sender ID, Inbound SMS webhooks, and Delivery receipts (DLR) webhooks. You need to fill the DLR webhook field with your full ngrok URL + path and select POST.

    Why this configuration? Now, whenever the delivery status of an SMS sent from your Vonage account changes, Vonage will make an HTTP POST request containing the DLR details to the public ngrok URL you provided. ngrok will then forward this request to your local Node.js application running on port 3000, specifically hitting the /webhooks/delivery-receipts route we will define next.

3. Implementing the Webhook Endpoint

Now, let's write the Express code in index.js to create the server and handle incoming DLR webhooks.

javascript
// index.js

// 1. Import Dependencies
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');

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

// 3. Middleware
// Enable Express to parse JSON request bodies.
// This is crucial because Vonage sends DLR data as JSON in the POST request body.
// This middleware makes the parsed JSON data available on req.body.
app.use(express.json());
// Enable Express to parse URL-encoded request bodies (e.g., from HTML forms, less common for DLRs).
app.use(express.urlencoded({ extended: true }));

// 4. Define Webhook Handler Function
const handleDeliveryReceipt = (req, res) => {
  // Vonage primarily sends DLR data via POST (in req.body).
  // Merging req.query allows handling potential GET requests too (e.g., for verification).
  const params = { ...req.query, ...req.body };

  // Note: Using console.log for simplicity in this example.
  // In production, replace these with a structured logger (see Section 6).
  console.log('--- Delivery Receipt Received ---');
  console.log('Timestamp:', new Date().toISOString());
  console.log('Payload:', JSON.stringify(params, null, 2)); // Pretty print the JSON

  // --- Your Application Logic Here ---
  // Example: Update database, trigger notifications, etc. based on 'params.status'
  const messageId = params.messageId;
  const status = params.status;
  const timestamp = params['message-timestamp']; // Or params.scts

  if (messageId && status) {
    console.log(`Processing DLR for Message ID: ${messageId}, Status: ${status}, Timestamp: ${timestamp}`);
    // TODO: Add logic to find the message in your database using messageId
    // and update its status to 'status'. Handle different statuses appropriately.
    // E.g., database.updateMessageStatus(messageId, status, params);
  } else {
    console.warn('Received webhook data does not look like a standard DLR:', params);
  }
  // --- End Application Logic ---

  // 5. IMPORTANT: Respond to Vonage with a 2xx Status Code
  // Acknowledge receipt of the webhook to prevent Vonage from retrying.
  // 204 No Content is appropriate as we don't need to send a body back.
  res.status(204).send();
};

// 6. Define Webhook Route
// Listen for both GET and POST requests on the path configured in Vonage Dashboard
app.route('/webhooks/delivery-receipts')
  .get(handleDeliveryReceipt)   // Handles potential verification GET requests
  .post(handleDeliveryReceipt); // Handles the actual DLR POST requests

// 7. Start the Server
app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
  console.log(`Webhook endpoint: http://localhost:${PORT}/webhooks/delivery-receipts`);
  console.log('Ensure ngrok is running and forwarding to this port.');
  console.log('Vonage DLR webhook URL should be set to your ngrok Forwarding URL + /webhooks/delivery-receipts');
});

Code Explanation:

  1. Dependencies: Loads dotenv to access environment variables and express.
  2. App Initialization: Creates an Express application instance and determines the port.
  3. Middleware:
    • express.json(): Parses incoming requests with JSON payloads and makes the data available on req.body. This is essential as Vonage sends DLRs in JSON format via POST requests.
    • express.urlencoded(): Parses incoming requests with URL-encoded payloads. While less common for DLRs, it's good practice to include for general webhook handling.
  4. handleDeliveryReceipt Function:
    • This function contains the core logic for processing the webhook.
    • It merges req.query and req.body into a single params object to handle data regardless of the HTTP method (though DLRs primarily use POST/req.body).
    • It logs the received payload for debugging. In production, you'd replace console.log with a proper logging library as detailed in Section 6.
    • Includes comments indicating where your specific application logic (e.g., database updates) should go. It extracts key fields like messageId and status.
  5. Acknowledge Receipt (Crucial): res.status(204).send(); sends an HTTP 204 No Content status back to Vonage. This confirms successful receipt. If you don't send a 2xx response, Vonage will assume the webhook failed and will retry sending it multiple times, potentially leading to duplicate processing in your application.
  6. Webhook Route: app.route('/webhooks/delivery-receipts').get(...).post(...) sets up the listener for the specific path (/webhooks/delivery-receipts) you configured in the Vonage dashboard. It routes both GET and POST requests to the same handleDeliveryReceipt function.
  7. Start Server: app.listen() starts the Express server, making it ready to accept incoming connections on the specified port.

4. Running and Testing the Application

Now, let's run the server and send an SMS to trigger a delivery receipt.

  1. Run the Node.js Application: In your project terminal (vonage-sms-dlr-webhook directory), start the server:

    bash
    node index.js

    You should see output confirming the server is running:

    Server listening on port 3000 Webhook endpoint: http://localhost:3000/webhooks/delivery-receipts Ensure ngrok is running and forwarding to this port. Vonage DLR webhook URL should be set to your ngrok Forwarding URL + /webhooks/delivery-receipts
  2. Ensure ngrok is Still Running: Double-check that your ngrok terminal window (from Step 2.3) is still active and forwarding traffic.

  3. Send a Test SMS: You need to send an SMS from your Vonage virtual number to a real, reachable mobile number (like your own cell phone). The most straightforward ways to do this for testing are:

    • Vonage API Dashboard: Navigate to ""API Reference"" > ""SMS API"" > ""Send SMS"" in the dashboard. Fill in your mobile number in the to field, your Vonage number in the from field, add some text, enter your API key and secret, and click ""Send request"". This uses the dashboard's built-in tool.
    • Vonage SDK: Use one of the official Vonage Server SDKs (like @vonage/server-sdk for Node.js) in a separate script or project to send the message programmatically. This is the recommended approach for actual applications as it handles authentication and API interaction more robustly.
    • API Client (e.g., Postman): Configure an API client to make a POST request to the Vonage Send SMS endpoint (refer to Vonage documentation for the current recommended endpoint, like the Messages API v1: https://api.nexmo.com/v1/messages). Ensure you use secure authentication methods (like Basic Auth with API key/secret in headers) rather than including credentials in the request body.

    Important: Ensure the SMS is sent using the same Vonage account (API Key) for which you configured the DLR webhook URL in the settings.

  4. Observe Webhook Activity:

    • ngrok Terminal: You should see an incoming POST request logged in the ngrok terminal shortly after the SMS is delivered (or fails). It will show POST /webhooks/delivery-receipts 204 No Content.
    • Node.js Terminal: Your running node index.js application should output the DLR payload received from Vonage.

    The output in your Node.js terminal will look similar to this (the exact fields might vary slightly, timestamps are examples):

    json
    --- Delivery Receipt Received ---
    Timestamp: 2023-10-27T11:45:10.123Z
    Payload: {
      ""msisdn"": ""YOUR_MOBILE_NUMBER"",
      ""to"": ""YOUR_VONAGE_NUMBER"",
      ""network-code"": ""310260"",
      ""messageId"": ""17000003B63A197B"",
      ""price"": ""0.00869000"",
      ""status"": ""delivered"",
      ""scts"": ""2310271145"",
      ""err-code"": ""0"",
      ""api-key"": ""YOUR_API_KEY"",
      ""message-timestamp"": ""2023-10-27 11:45:05""
    }
    Processing DLR for Message ID: 17000003B63A197B, Status: delivered, Timestamp: 2023-10-27 11:45:05

    Key DLR Fields:

    • messageId: The unique ID of the SMS message. Use this to correlate the DLR with the message you originally sent.
    • status: The delivery status (e.g., delivered, failed, expired, rejected, accepted, buffered). This is the most critical field.
    • msisdn: The recipient's phone number.
    • to: The sender ID or Vonage number used.
    • err-code: Error code if the status is failed or rejected. 0 usually indicates success.
    • message-timestamp or scts: Timestamp related to the status update from the carrier.
    • network-code: Identifies the recipient's mobile network carrier.
    • price: The cost charged for the message segment.
    • api-key: The Vonage API key associated with the sent message.

5. Handling Different Delivery Statuses

The status field in the DLR payload tells you the final outcome of the message. Your webhook logic should handle these appropriately.

Common Statuses:

  • delivered: The message was successfully delivered to the recipient's handset. This is the ideal outcome.
  • accepted: The message was accepted by the downstream carrier but final delivery status is not yet known or available.
  • buffered: The message is temporarily held by the network (e.g., handset offline) and delivery will be retried.
  • failed: The message could not be delivered (e.g., invalid number, network issue, phone blocked). Check err-code for details.
  • expired: The message could not be delivered within the carrier's validity period (often 24-72 hours).
  • rejected: The message was rejected by the carrier or Vonage (e.g., spam filtering, destination blocked). Check err-code.
  • unknown: The final status could not be determined.

Refer to the Vonage SMS API Documentation - Delivery Receipts for a complete list and detailed explanations.

Example Logic (Conceptual):

In your handleDeliveryReceipt function, after parsing params:

javascript
// Inside handleDeliveryReceipt function...

const messageId = params.messageId;
const status = params.status;
const errorCode = params['err-code'];
const timestamp = params['message-timestamp'] || params.scts; // Use available timestamp

if (messageId && status) {
    console.log(`Processing DLR for Message ID: ${messageId}, Status: ${status}`);

    // --- Example Database Interaction (Conceptual) ---
    // Assume you have an async function like:
    // async function updateMessageStatusInDb(id, status, details) { ... }
    try {
        // Example: await updateMessageStatusInDb(messageId, status, { errorCode, timestamp, rawPayload: params });

        switch (status) {
            case 'delivered':
                console.log(`Message ${messageId} confirmed delivered.`);
                // Trigger success actions, update UI, etc.
                break;
            case 'failed':
            case 'rejected':
                console.error(`Message ${messageId} failed/rejected. Error Code: ${errorCode}`);
                // Trigger failure actions: notify support, retry logic (carefully!), mark as failed.
                // Consider logging the full payload for debugging failed messages.
                // Example: await logDeliveryFailure(messageId, status, errorCode, params);
                break;
            case 'expired':
                console.warn(`Message ${messageId} expired.`);
                // Mark as undelivered after timeout.
                break;
            case 'accepted':
            case 'buffered':
                console.log(`Message ${messageId} is currently ${status}. Awaiting final status.`);
                // Optional: Update status but wait for a final DLR ('delivered', 'failed', etc.)
                // Some applications might treat 'accepted' as provisionally successful.
                break;
            default:
                console.log(`Received unhandled status '${status}' for message ${messageId}`);
        }
    } catch (error) {
        console.error(`Error processing DLR for ${messageId}:`, error);
        // IMPORTANT: Still need to send 2xx response even if DB or internal logic fails here!
        // Log the error details (including messageId and payload) for later investigation/retry.
    }
    // --- End Example Logic ---

} else {
    console.warn('Received webhook data does not look like a standard DLR:', params);
}

// Send 2xx response regardless of processing outcome to ACK receipt
res.status(204).send();

6. Error Handling and Logging

Production applications require more robust error handling and structured logging than simple console.log.

Error Handling Strategy:

  • Wrap Core Logic: Use try...catch blocks around your database interactions or other critical processing within the webhook handler.
  • Log Errors: If an error occurs during processing (e.g., database connection issue), log the error details comprehensively (including the DLR payload that caused it) using a proper logging library.
  • Always Respond 2xx: Crucially, even if your internal processing fails, your webhook must still return a 200 OK or 204 No Content to Vonage. This acknowledges receipt. Handle the processing failure asynchronously (e.g., retry mechanism with exponential backoff, dead-letter queue). Failure to respond 2xx causes Vonage retries, potentially exacerbating the problem.

Logging:

Replace console.log with a structured logging library like pino or winston.

Example using pino:

  1. Install pino:
    bash
    npm install pino pino-pretty # pino-pretty for development readability
  2. Configure logger (e.g., create logger.js):
    javascript
    // logger.js
    const pino = require('pino');
    
    const logger = pino({
      level: process.env.LOG_LEVEL || 'info',
      // Use pino-pretty only in development for better readability
      transport: process.env.NODE_ENV !== 'production' ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
          ignore: 'pid,hostname', // Optional: Hide process ID and hostname
        }
      } : undefined, // In production, use default JSON output
    });
    
    module.exports = logger;
  3. Use logger in index.js:
    javascript
    // index.js
    // ... other imports
    const logger = require('./logger'); // Assuming logger.js is created
    
    // ... inside handleDeliveryReceipt
    // Replace console.log with logger calls
    logger.info({ payload: params }, 'Delivery Receipt Received'); // Log object payload
    
    if (messageId && status) {
        logger.info({ messageId, status, timestamp }, `Processing DLR`); // Log key fields
        // ... your logic
    } else {
        logger.warn({ payload: params }, 'Received non-standard DLR data');
    }
    
    // Example error logging within a try/catch
    try {
      // ... your processing logic ...
      if (status === 'failed' || status === 'rejected') {
        logger.error({ messageId, status, errorCode, payload: params }, `Message delivery failed or rejected`);
      }
      // ... other status handling
    } catch (error) {
      // Log the error object along with context
      logger.error({ err: error, messageId, payload: params }, `Error processing DLR`);
      // Still send 2xx response below!
    }
    // ...
    
    // Replace server start console.log
    app.listen(PORT, () => {
      logger.info(`Server listening on port ${PORT}`);
      logger.info(`Webhook endpoint: http://localhost:${PORT}/webhooks/delivery-receipts`);
      // ... other startup logs
    });

This provides structured JSON logs in production (good for log aggregation tools like Datadog, Splunk, ELK stack) and human-readable logs during development.

7. Security Considerations

Webhooks are public-facing endpoints, so securing them is important.

  1. Use HTTPS: Always use https:// for your webhook URL (ngrok provides this automatically for development). In production, ensure your server is configured for HTTPS using valid TLS/SSL certificates (e.g., via Let's Encrypt or your hosting provider).
  2. Validate Incoming Requests: Standard Vonage SMS DLR webhooks do not typically use verifiable signatures (like JWT or HMAC signatures found in some other Vonage APIs or other webhook providers). Therefore, validation options are limited but still recommended:
    • Check api-key (Basic Check, Not Foolproof): The DLR payload includes the api-key associated with the message. You could check if this matches your expected Vonage API key (process.env.VONAGE_API_KEY). However, this is not strong security: the API key itself isn't secret if the transport isn't secure (hence HTTPS is vital), and simply checking the key doesn't guarantee the request came from Vonage (it could be replayed) or prevent tampering if the connection wasn't secure end-to-end. Treat this as a basic sanity check, not a primary security mechanism.
    • IP Whitelisting: If Vonage publishes a stable list of IP addresses or ranges from which webhooks originate (check their current documentation), you could configure your firewall or application middleware to only accept requests from those IPs. This adds a layer of defense but can be complex to maintain if the IPs change.
    • Secret Path (Obscurity): Using a long, unpredictable, randomly generated path for your webhook (e.g., /webhooks/dlr/aBcD3fGhIjK1mN2oPqR3sT4uV5wX6yZ7) makes it harder for attackers to guess your endpoint URL. This is security through obscurity and should not be relied upon alone.
  3. Rate Limiting: Implement rate limiting on your webhook endpoint (using libraries like express-rate-limit) to protect against brute-force attacks, accidental loops, or denial-of-service (DoS) attempts. Configure sensible limits based on expected traffic.
  4. Input Validation: Although you are primarily receiving data defined by Vonage, validate the presence and basic format of expected key fields (messageId, status) before processing. This prevents errors if the payload structure changes unexpectedly or if malformed requests are received. Log any validation failures.
  5. Check Vonage Documentation for Signed Webhooks: While standard SMS DLRs often lack signatures, Vonage is continuously evolving its APIs. Always check the latest Vonage developer documentation to see if signed webhooks (e.g., using JWT or HMAC) are available for the specific API or configuration you are using, especially newer APIs like the Messages API. If available, implementing signature verification is a much stronger security measure than the methods above.

For many use cases, HTTPS combined with rate limiting and potentially IP whitelisting provides a reasonable security baseline for standard DLRs. If strong guarantees of authenticity and integrity are required, investigate signed webhook options in the latest Vonage documentation.

8. Troubleshooting and Caveats

  • Webhook Not Triggering:
    • ngrok: Is ngrok running? Is the Forwarding URL correct in Vonage? Has the ngrok session expired or the URL changed (restarting ngrok often changes the URL)?
    • Vonage Config: In the Vonage Dashboard (Settings -> SMS settings -> DLR webhooks), is the URL exactly correct (must start with https://, include your specific path like /webhooks/delivery-receipts, and have the method set to POST)? Are changes saved?
    • Firewall: Is there any network firewall (on your machine, router, or cloud provider) blocking incoming requests to the port ngrok forwards to (e.g., 3000) or your production server's port?
    • SMS Sent Correctly? Did you send the test SMS from the Vonage number associated with your API key/account where the webhook is configured?
    • Server Running? Is your node index.js application actually running and listening on the correct port without crashing? Check startup logs.
  • Receiving Requests but Empty Payload (req.body is empty/undefined):
    • Middleware Order: Ensure the app.use(express.json()); middleware is correctly included in index.js and, crucially, placed before your route definition (app.route('/webhooks/delivery-receipts')...).
    • Content-Type Header: Check the Content-Type header of the incoming request from Vonage. Use the ngrok web interface (http://127.0.0.1:4040) or detailed logging to inspect the request headers. It should be application/json for express.json() to parse it. If it's something else (e.g., application/x-www-form-urlencoded), you might need express.urlencoded() instead or investigate why Vonage is sending a different content type.
  • Vonage Keeps Retrying the Webhook (Multiple Identical Requests):
    • Missing 2xx Response: You are most likely not sending a successful HTTP status code (like 200 OK or 204 No Content) back to Vonage quickly enough or at all. Ensure res.status(204).send(); (or similar res.sendStatus(204)) is called reliably at the end of your handler function, even if your internal processing encounters an error. Handle internal errors asynchronously if needed, but always acknowledge the webhook receipt to Vonage.
    • Timeout: Your processing logic might be taking too long. Vonage expects a response within a certain timeout period (check their documentation, often a few seconds). If your handler takes too long, Vonage might consider it failed and retry. Offload long-running tasks.
  • Not Receiving DLRs for All Messages/Carriers:
    • DLR Support Varies: Delivery receipt reliability is heavily dependent on the destination country and the specific mobile carrier network. Not all networks provide timely or accurate DLRs back to Vonage, and some may not provide them at all. This is an inherent limitation of SMS technology in some regions. Check Vonage's country-specific features and restrictions documentation.
    • Message Type: DLR behavior might differ for standard SMS versus messages sent via other channels (like WhatsApp or Viber) using Vonage's Messages API. Consult the specific API documentation.

Frequently Asked Questions

How to set up SMS delivery receipt webhooks with Vonage?

Set up a webhook endpoint in your application to receive DLRs. Configure your Vonage account settings to send DLRs to this endpoint. Use a tool like ngrok to expose your local development server during testing. Ensure your endpoint responds with a 2xx status code to acknowledge receipt.

What is a Delivery Receipt (DLR) in Vonage?

A DLR is a notification from Vonage that provides the delivery status of an SMS message. It confirms whether the message was successfully delivered, failed, or encountered another status like "buffered" or "expired". This allows for real-time tracking and handling of message delivery outcomes.

Why do I need DLR webhooks for SMS?

DLR webhooks provide real-time delivery status updates, going beyond the initial confirmation from the Vonage API. This enables accurate delivery tracking, handling failed messages, and gathering analytics on SMS campaign effectiveness, leading to a more robust and user-friendly application.

How to handle different Vonage DLR statuses?

Use a switch statement or similar logic in your webhook handler to process different statuses such as 'delivered', 'failed', 'expired', etc. 'delivered' indicates success. 'failed' might require retries or support notifications. Check the 'err-code' for failure reasons. Always respond with a 2xx status code, even if your internal handling fails.

What are the Vonage DLR status codes?

Common statuses include 'delivered', 'failed', 'expired', 'rejected', 'accepted', 'buffered', and 'unknown'. Consult Vonage documentation for the complete list and details. 'delivered' signifies successful delivery. 'failed' implies delivery failure. 'buffered' means temporary holding by the network.

How can I test my Node.js Vonage DLR webhook?

Run your Node.js application and ensure ngrok is forwarding to the correct port. Send a test SMS from your Vonage number to a real mobile number. Observe the ngrok terminal for incoming webhook requests and the Node.js console for the DLR payload. Ensure your Vonage account settings point to the correct ngrok URL.

How to secure Vonage SMS delivery receipt webhook?

Secure your webhooks using HTTPS and a non-guessable URL path. While not foolproof, checking the 'api-key' in the payload against your Vonage key provides a basic verification step. Implement rate limiting to prevent abuse. Consult Vonage documentation for signed webhook options for added security.

When to use express.json() for Vonage DLR?

Use `express.json()` middleware *before* defining your webhook route. It's crucial because Vonage sends DLR data as JSON in the POST request body. This middleware parses incoming JSON payloads and makes the data accessible on `req.body` for processing within your route handler.

Why does Vonage keep retrying my webhook?

Vonage retries webhooks if it doesn't receive a 2xx success status code (like 200 or 204) within its timeout period. Ensure your endpoint responds with a success code even if your application logic has errors. Handle any errors asynchronously after acknowledging receipt.

What to do if Vonage webhook is not being triggered?

Verify that ngrok is running and your Vonage settings are pointed to the correct ngrok forwarding URL, including the path. Confirm that the SMS was sent from the Vonage number linked to the configured API key. Check firewall settings. Ensure the server is running and your webhook route is correctly defined.

What is the role of ngrok with Vonage webhooks?

Ngrok creates a secure tunnel from a public URL to your local development server. This allows Vonage to send webhook requests to your application running locally during development, bypassing the need for a publicly accessible server.

What is the purpose of the 'err-code' in Vonage DLR?

The 'err-code' field in the DLR payload provides specific error information when the 'status' is 'failed' or 'rejected'. A value of '0' generally indicates success. Other codes indicate different failure reasons, which can be found in the Vonage API documentation.

How to troubleshoot empty body in Vonage webhook?

Ensure `express.json()` middleware is used *before* defining your webhook route. Confirm the 'Content-Type' of the incoming request is 'application/json' using your ngrok inspection interface or server-side logging.

Can I use Vonage DLR with other messaging channels?

While the standard DLR applies to SMS, DLR behavior might differ for other channels like WhatsApp or Viber used through the Vonage Messages API. Refer to the specific channel's documentation within the Messages API for details on delivery receipts.

Why are Vonage DLRs not received for all messages?

DLR reliability varies by country and carrier network. Not all networks provide timely or accurate DLRs, and some don't offer them at all. This is a limitation of SMS technology itself. Vonage documentation provides details on country-specific capabilities.