Frequently Asked Questions
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Technologies Involved:
.env
file intoprocess.env
, keeping sensitive credentials out of source code.System Architecture:
Prerequisites:
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for the project, then navigate into it.
Initialize Node.js Project: This creates a
package.json
file to manage project dependencies and scripts.Install Dependencies: We need
express
for the web server, anddotenv
to manage environment variables securely.express
: The web framework.dotenv
: Loads environment variables from a.env
file.Create Core Files: Create the main application file and files for environment variables and Git ignore rules.
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing dependencies and sensitive credentials to version control.Set Up Environment Variables (
.env
): Open the.env
file and add placeholders for your Vonage credentials and the port your server will run 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 use3000
in this guide.Project Structure: Your basic project structure should now look like this:
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.Retrieve Vonage Credentials:
.env
file, replacingYOUR_API_KEY
andYOUR_API_SECRET
.Ensure You Have a Vonage Number:
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).ngrok
will display output similar to this:https://
Forwarding URL (e.g.,https://xxxxxxxxxxxx.ngrok.io
). This is your temporary public URL. Keep thisngrok
terminal window running. If you stop and restart ngrok, you will get a new URL and must update the Vonage configuration.Configure Vonage DLR Webhook URL:
ngrok
https://
Forwarding URL into the field./webhooks/delivery-receipts
. The full URL will look like:https://xxxxxxxxxxxx.ngrok.io/webhooks/delivery-receipts
POST
. Vonage sends DLR data in the request body via POST.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.Code Explanation:
dotenv
to access environment variables andexpress
.express.json()
: Parses incoming requests with JSON payloads and makes the data available onreq.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.handleDeliveryReceipt
Function:req.query
andreq.body
into a singleparams
object to handle data regardless of the HTTP method (though DLRs primarily use POST/req.body
).console.log
with a proper logging library as detailed in Section 6.messageId
andstatus
.res.status(204).send();
sends an HTTP204 No Content
status back to Vonage. This confirms successful receipt. If you don't send a2xx
response, Vonage will assume the webhook failed and will retry sending it multiple times, potentially leading to duplicate processing in your application.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 samehandleDeliveryReceipt
function.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.
Run the Node.js Application: In your project terminal (
vonage-sms-dlr-webhook
directory), start the server:You should see output confirming the server is running:
Ensure
ngrok
is Still Running: Double-check that yourngrok
terminal window (from Step 2.3) is still active and forwarding traffic.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:
to
field, your Vonage number in thefrom
field, add sometext
, enter your API key and secret, and click ""Send request"". This uses the dashboard's built-in tool.@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.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.
Observe Webhook Activity:
ngrok
Terminal: You should see an incoming POST request logged in thengrok
terminal shortly after the SMS is delivered (or fails). It will showPOST /webhooks/delivery-receipts 204 No Content
.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):
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 isfailed
orrejected
.0
usually indicates success.message-timestamp
orscts
: 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). Checkerr-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). Checkerr-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 parsingparams
:6. Error Handling and Logging
Production applications require more robust error handling and structured logging than simple
console.log
.Error Handling Strategy:
try...catch
blocks around your database interactions or other critical processing within the webhook handler.2xx
: Crucially, even if your internal processing fails, your webhook must still return a200 OK
or204 No Content
to Vonage. This acknowledges receipt. Handle the processing failure asynchronously (e.g., retry mechanism with exponential backoff, dead-letter queue). Failure to respond2xx
causes Vonage retries, potentially exacerbating the problem.Logging:
Replace
console.log
with a structured logging library likepino
orwinston
.Example using
pino
:pino
:logger.js
):index.js
: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.
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).api-key
(Basic Check, Not Foolproof): The DLR payload includes theapi-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./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.express-rate-limit
) to protect against brute-force attacks, accidental loops, or denial-of-service (DoS) attempts. Configure sensible limits based on expected traffic.messageId
,status
) before processing. This prevents errors if the payload structure changes unexpectedly or if malformed requests are received. Log any validation failures.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
ngrok
: Isngrok
running? Is the Forwarding URL correct in Vonage? Has thengrok
session expired or the URL changed (restartingngrok
often changes the URL)?https://
, include your specific path like/webhooks/delivery-receipts
, and have the method set toPOST
)? Are changes saved?ngrok
forwards to (e.g., 3000) or your production server's port?node index.js
application actually running and listening on the correct port without crashing? Check startup logs.req.body
is empty/undefined):app.use(express.json());
middleware is correctly included inindex.js
and, crucially, placed before your route definition (app.route('/webhooks/delivery-receipts')...
).Content-Type
Header: Check theContent-Type
header of the incoming request from Vonage. Use thengrok
web interface (http://127.0.0.1:4040
) or detailed logging to inspect the request headers. It should beapplication/json
forexpress.json()
to parse it. If it's something else (e.g.,application/x-www-form-urlencoded
), you might needexpress.urlencoded()
instead or investigate why Vonage is sending a different content type.2xx
Response: You are most likely not sending a successful HTTP status code (like200 OK
or204 No Content
) back to Vonage quickly enough or at all. Ensureres.status(204).send();
(or similarres.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.