Vonage SMS Delivery Status & Webhooks with Node.js Express: Complete Guide
Learn how to send SMS messages, receive inbound texts, and track delivery status using Vonage Messages API with Node.js and Express. Includes webhook implementation and real-time status tracking.
This comprehensive guide shows you how to build a production-ready Node.js application using Express to send SMS messages, receive inbound SMS, and track delivery status in real-time using Vonage Messages API webhooks.
Learn how to implement SMS delivery tracking and webhooks in Node.js for:
Sending SMS messages programmatically: Trigger SMS messages to specified recipients via an API endpoint using the Vonage Messages API.
Receiving inbound SMS messages: Handle incoming texts sent to your Vonage virtual number using webhook callbacks.
Tracking SMS delivery status in real-time: Monitor delivery receipts (DLR) and receive instant status updates (delivered, failed, rejected) via webhook notifications.
This tutorial solves the common need for applications to reliably communicate with users via SMS and track message delivery confirmation. Perfect for building two-factor authentication (2FA), notification systems, and SMS marketing campaigns. Note: Test the code snippets rigorously in your environment, especially error handling and security implementations.
Technologies Used
Node.js: A JavaScript runtime environment for server-side development. Requires Node.js 14+ or later.
Express: A minimal and flexible Node.js web application framework.
Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. This guide uses it for comprehensive features, JWT-based webhook security, and webhook capabilities for inbound messages and status updates.
@vonage/server-sdk: The official Vonage Node.js SDK (v3.x or later) for interacting with Vonage APIs.
dotenv: A module to load environment variables from a .env file.
jsonwebtoken: (Conceptually needed for JWT verification, though implementation details are deferred to Vonage docs).
ngrok: A tool to expose local development servers to the internet for testing webhooks during development.
System Architecture: How SMS Webhooks Work
The SMS delivery tracking system involves several components interacting:
Your Application (Node.js/Express): Hosts the API endpoint for sending SMS and the webhook endpoints for receiving inbound messages and delivery status callbacks.
Vonage Platform: Provides the Messages API, virtual numbers, and handles the routing of SMS messages and webhook events.
User's Mobile Device: Sends and receives SMS messages.
ngrok (Development): Tunnels requests from a public URL to your local development server. For production, use a stable public IP/domain with SSL (see Production Deployment section).
Prerequisites
Before starting, ensure you have the following:
Vonage API Account: Sign up for free at Vonage API Dashboard. You'll get free credit to start.
Vonage API Key and Secret: Find these at the top of your Vonage API Dashboard. (Required for SDK setup, though Application ID/Private Key is primary auth for Messages API calls).
Vonage Virtual Number: Purchase an SMS-capable number from the dashboard under "Numbers" > "Buy Numbers".
Node.js and npm (or yarn): Installed on your system. Download from nodejs.org. Requires Node.js 14 or later.
ngrok: Installed and authenticated. Download from ngrok.com. A free account is sufficient for development. Note that free ngrok URLs are temporary. Paid tiers offer stable domains.
Basic understanding of Node.js, Express, and APIs.
1. Setting up the Project
Initialize your Node.js project and install the necessary dependencies.
Create Project Directory:
Open your terminal and create a new directory for your project, then navigate into it.
Bash
mkdir vonage-messages-guide
cd vonage-messages-guide
Initialize Node.js Project:
This creates a package.json file to manage dependencies and project metadata.
Bash
npm init -y
(Use yarn init -y if you prefer Yarn)
Install Dependencies:
Install express for the web server, @vonage/server-sdk to interact with the Vonage API, and dotenv to manage environment variables. Also install jsonwebtoken for webhook security.
Set up Environment Variables (.env):
Open the .env file and add the following variables. Fill in the values in the next steps.
.env
# Vonage Credentials
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Your purchased Vonage number
# Application Config
PORT=3000 # Port your local server will run on
BASE_WEBHOOK_URL=YOUR_NGROK_FORWARDING_URL # We'll get this from ngrok later
VONAGE_API_KEY, VONAGE_API_SECRET: Found on your Vonage dashboard. Used by the SDK, though Application ID/Private Key is primary for Messages API.
VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH: Obtained when creating a Vonage Application (next section).
VONAGE_NUMBER: Your purchased Vonage virtual phone number in E.164 format (e.g., 14155552671 for US, 447700900000 for UK).
PORT: The local port your Express application will listen on.
BASE_WEBHOOK_URL: The public URL provided by ngrok (or your production server URL).
Basic Express Server (server.js):
Set up a minimal Express server to ensure everything works.
JavaScript
// server.jsrequire('dotenv').config(); // Load .env variables into process.envconst express = require('express');
const app = express();
// Middleware to parse JSON bodies// Vonage Messages API webhooks typically use JSON
app.use(express.json());
// Include urlencoded parser for flexibility, though JSON is primary for Messages API
app.use(express.urlencoded({ extended: true }));
constPORT = process.env.PORT || 3000;
// Simple root route for testing
app.get('/', (req, res) => {
res.send('Vonage Messages API Guide App is running!');
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', timestamp: newDate().toISOString() });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
Run this with node server.js. You should see the confirmation message in your console. Accessing http://localhost:3000 in your browser should show the test message.
2. Vonage Account and Application Setup
Configure your Vonage account and create a Vonage Application to handle Messages API interactions.
Your API key and API secret are displayed prominently at the top.
Copy these values into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET.
Purchase a Vonage Number:
Navigate to Numbers > Buy numbers.
Search for a number with SMS capabilities in your desired country.
Purchase the number.
Copy the purchased number (in E.164 format, e.g., 12015550123) into your .env file for VONAGE_NUMBER.
Set Default SMS API to "Messages API":
This is crucial for consistency. Navigate to Settings in the dashboard.
Scroll down to SMS settings.
Ensure that Default SMS Setting is set to Messages API. If it's set to "SMS API", toggle it to "Messages API".
Click Save changes.
Why? While linking a number to a Messages-enabled Application should force Messages API usage for that app, this account-wide setting ensures consistent Messages API behavior (and webhook formats) for numbers not linked to this specific application or if other authentication methods are ever used inadvertently within your account. It aligns your account's default behavior with this guide's focus.
Create a Vonage Application:
Vonage Applications act as containers for configuration, security credentials (like the private key), and associated numbers, specifically for APIs like Messages.
Navigate to Applications > Create a new application.
Give your application a descriptive Name (e.g., "Node Messages Guide App").
Click Generate public and private key. This will automatically download the private.key file. Save this file securely in the root of your project directory (matching VONAGE_PRIVATE_KEY_PATH in your .env). Crucially, add private.key or *.pem to your .gitignore file. Vonage stores the public key; you keep the private key to authenticate SDK requests using JWT.
Enable the Messages capability by toggling it on.
Enter webhook endpoints using your base URL variable (update these with the real ngrok URL later):
Status URL:${BASE_WEBHOOK_URL}/webhooks/status (Method: POST)
Scroll down to Link virtual numbers and link the Vonage number you purchased earlier to this application. Select the number and click Link.
Click Generate new application.
You will be redirected to the application's details page. Copy the Application ID.
Paste the Application ID into your .env file for VONAGE_APPLICATION_ID.
3. Setting up SMS Webhooks with ngrok (Development)
Webhooks enable real-time SMS delivery tracking by allowing Vonage to send instant notifications (inbound messages, delivery status updates) to your application. Since your application runs locally during development, use ngrok to create a secure tunnel from a public URL to your localhost.
Important: ngrok is excellent for development and testing, but not suitable for production. Production environments require a stable, publicly accessible IP address or domain name with a valid SSL/TLS certificate. See the Production Deployment section for more details.
Run ngrok:
Open a new terminal window (keep your Node.js server running or be ready to restart it). Run ngrok, telling it to forward traffic to the port your Express app listens on (defined in .env, default is 3000).
Bash
ngrok http 3000
Get the Forwarding URL:
ngrok will display session information, including a public Forwarding URL (usually ending in .ngrok.io or .ngrok-free.app). It will look something like https://<random-string>.ngrok-free.app. Always use the https version.
Code
Session Status online
Account Your Name (Plan: Free/Paid)
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 this https://... Forwarding URL. Note that free accounts generate a new random URL each time ngrok starts. Paid ngrok plans offer stable subdomains.
Tip: Access the ngrok web interface at http://127.0.0.1:4040 to inspect all incoming requests, view headers (including JWT tokens), and debug webhook issues.
Update .env:
Paste the copied ngrok Forwarding URL into your .env file for the BASE_WEBHOOK_URL variable.
Status URL:https://<random-string>.ngrok-free.app/webhooks/status
Click Save changes.
Why? When Vonage needs to send an inbound message or a status update related to this application, it sends an HTTP POST request to these public ngrok URLs, which ngrok forwards to your locally running Express application's /webhooks/inbound and /webhooks/status routes.
Restart Your Node.js Server:
If your server is running, stop it (Ctrl+C) and restart it (node server.js) to ensure it picks up the updated .env configuration.
4. Implementing SMS Sending
Create the Vonage SDK client and add an endpoint to send SMS messages using the Messages API.
Configure Vonage SDK Client (vonageClient.js):
This module initializes the Vonage client using credentials from the .env file. Using Application ID and Private Key is the recommended authentication method for the Messages API as it uses JWTs.
JavaScript
// vonageClient.jsrequire('dotenv').config();
const { Vonage } = require('@vonage/server-sdk');
const { readFileSync } = require('fs'); // To read the private key file// Read the private key from the file path specified in .envlet privateKeyValue;
try {
privateKeyValue = readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (error) {
console.error(`Error reading private key from path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`, error);
process.exit(1); // Exit if private key cannot be loaded
}
const vonage = newVonage({
apiKey: process.env.VONAGE_API_KEY, // Still required by SDK constructorapiSecret: process.env.VONAGE_API_SECRET, // Still required by SDK constructorapplicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKeyValue // Use the key content read from the file
}, {
// Optional: Set custom options here// apiHost: 'https://messages-sandbox.nexmo.com' // Example: Use the sandbox
});
module.exports = vonage;
Why use Application ID/Private Key? This method uses JWTs generated by the SDK for authenticating API requests, which is more secure than sending API Key/Secret with each request for server-to-server communication via the Messages API.
Create Send SMS Endpoint (server.js):
Add a new route to your server.js file to handle sending SMS messages. Make it a POST request that accepts the recipient number and message text in the request body.
JavaScript
// server.js (add these parts)const vonage = require('./vonageClient'); // Import the initialized client// Keep existing setup: require('dotenv').config(), express, app, middleware...
app.get('/', (req, res) => { // Keep existing root route
res.send('Vonage Messages API Guide App is running!');
});
app.get('/health', (req, res) => { // Keep health check route
res.status(200).json({ status: 'healthy', timestamp: newDate().toISOString() });
});
// --- New Route for Sending SMS ---
app.post('/send-sms', async (req, res) => {
// Basic input validationconst { to, text } = req.body;
if (!to || !text) {
return res.status(400).json({ error: 'Missing "to" or "text" in request body.' });
}
// Consider adding phone number validation using libphonenumber-js in productionconst fromNumber = process.env.VONAGE_NUMBER;
if (!fromNumber) {
console.error('VONAGE_NUMBER is not set in .env');
return res.status(500).json({ error: 'Server configuration error: Missing Vonage number.' });
}
console.log(`Attempting to send SMS via Messages API from ${fromNumber} to ${to}`);
try {
// Use the vonage.messages.send() method for the Messages APIconst resp = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: to, // Recipient phone number (E.164 format recommended)from: fromNumber, // Your Vonage virtual number linked to the Applicationtext: text // The message content (max 160 chars for single SMS)
});
console.log('Message submission accepted by Vonage:', resp);
// The resp object contains the message_uuid
res.status(200).json({ success: true, message_uuid: resp.message_uuid });
} catch (err) {
console.error('Error sending SMS via Messages API:', err.response ? JSON.stringify(err.response.data, null, 2) : err.message);
// Provide more context if available from the Vonage error responseconst errorMessage = err.response?.data?.title || err.message || 'Failed to send SMS';
const errorDetail = err.response?.data?.detail || JSON.stringify(err.response?.data); // Include full detail if possible
res.status(err.response?.status || 500).json({
success: false,
error: errorMessage,
detail: errorDetail
});
}
});
// --- End of New Route ---// ... (Add webhook routes later)// Start the server (keep existing)constPORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
Note: SMS messages longer than 160 characters automatically concatenate into multiple segments. Each segment is billed separately. Consider truncating or paginating long messages.
Test Sending SMS:
Make sure your Node.js server and ngrok are running. Use curl or a tool like Postman/Insomnia to send a POST request to your /send-sms endpoint. Replace <your-ngrok-url> with your actual ngrok Forwarding URL and <recipient-phone-number> with a real phone number (in E.164 format, e.g., 14155552671).
Bash
curl -X POST <your-ngrok-url>/send-sms \
-H "Content-Type: application/json" \
-d '{
"to": "<recipient-phone-number>",
"text": "Hello from Vonage Messages API and Node.js!"
}'
You should receive an SMS on the recipient phone shortly. Your server console will log the success response from Vonage, including the message_uuid, which is crucial for tracking. The API call should return a JSON response like {"success":true,"message_uuid":"<some-uuid>"}. If errors occur, check the console logs for details from the catch block.
Common Errors:
401 Unauthorized: Check your Application ID and private key path in .env.
403 Forbidden: Ensure your number is linked to the Vonage Application.
422 Unprocessable Entity: Verify phone numbers are in E.164 format and the "to" number is valid.
5. Implementing Inbound SMS Webhook (Receive SMS)
This webhook endpoint handles incoming SMS messages sent to your Vonage virtual number. When someone texts your number, Vonage forwards the message to your webhook URL via HTTP POST request.
Create Inbound Webhook Route (server.js):
Add a POST route handler for the /webhooks/inbound path you configured in your Vonage Application.
JavaScript
// server.js (add this route)// ... (existing code: imports, middleware, /send-sms route)// --- New Route for Inbound SMS ---
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound SMS Webhook Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// Log headers for debugging signature verification laterconsole.log('Request Headers:', JSON.stringify(req.headers, null, 2));
// --- Security Check Placeholder ---// IMPORTANT: Implement JWT verification here for production!// See Security Considerations section and Vonage documentation.// verifyVonageSignature(req); // This function needs proper implementation// Process the inbound message (assuming signature verification passed or is deferred)const message = req.body;
// Check structure for typical Messages API inbound SMSif (message.from?.type === 'sms' && message.message?.content?.type === 'text') {
console.log(`From: ${message.from.number}`);
console.log(`To: ${message.to.number}`); // Your Vonage numberconsole.log(`Text: ${message.message.content.text}`);
console.log(`Message UUID: ${message.message_uuid}`);
console.log(`Timestamp: ${message.timestamp}`);
// --- Add your business logic here ---// Example: Store the message, trigger a reply, etc.// e.g., replyToSms(message.from.number, 'Thanks for your message!');
} else {
console.warn('Received webhook does not appear to be a standard inbound SMS via Messages API.');
}
// Vonage needs a 200 OK response quickly to acknowledge receipt.// Do complex processing asynchronously if needed.
res.status(200).end();
});
// --- End of Inbound Route ---// ... (Add status webhook route next)// Start the server (keep existing)// ...
Understanding Webhook Signature Verification (JWT):
The Messages API secures webhooks using JWT (JSON Web Tokens) attached to the Authorization header (typically Bearer <token>). Verifying this signature is critical for production security to ensure requests genuinely come from Vonage.
JWT Verification Steps:
Extract the JWT from the Authorization: Bearer <token> header
Decode the JWT to get the payload and claims
Fetch Vonage's public key (available via Vonage API or dashboard)
Verify the signature using jsonwebtoken.verify(token, publicKey, { algorithms: ['RS256'] })
Validate the claims (e.g., iat, exp, aud)
Refer to the official Vonage Developer Documentation on "Signed Webhooks" or "Webhook Security" specifically for the Messages API for the authoritative and up-to-date implementation guide.
Test Inbound SMS:
Ensure your Node.js server and ngrok are running.
Using your mobile phone, send an SMS message to your Vonage virtual number (VONAGE_NUMBER).
Watch your server console. You should see the "--- Inbound SMS Webhook Received ---" log message, followed by the parsed request body and headers. Verify the message content and sender number are logged correctly.
Check the ngrok web interface (http://127.0.0.1:4040) to inspect the incoming POST request to /webhooks/inbound, including the Authorization header containing the JWT.
6. Implementing Delivery Status Webhook (Track SMS Delivery)
This webhook receives real-time delivery receipt (DLR) updates about the delivery status of outbound messages you sent using the Messages API. Track whether your SMS was delivered, failed, or rejected by the carrier.
Create Status Webhook Route (server.js):
Add a POST route handler for the /webhooks/status path.
JavaScript
// server.js (add this route)// ... (existing code: imports, middleware, /send-sms, /webhooks/inbound)// --- New Route for Delivery Status Updates ---
app.post('/webhooks/status', (req, res) => {
console.log('--- Delivery Status Webhook Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// Log headers for debugging signature verification laterconsole.log('Request Headers:', JSON.stringify(req.headers, null, 2));
// --- Security Check Placeholder ---// IMPORTANT: Implement JWT verification here for production!// Use the same logic/mechanism as for the inbound webhook.// See Security Considerations section and Vonage documentation.// verifyVonageSignature(req); // This function needs proper implementation// Process the status update (assuming signature verification passed or is deferred)const statusUpdate = req.body;
// Key fields in a Messages API status updateif (statusUpdate.message_uuid && statusUpdate.status) {
console.log(`Status for message ${statusUpdate.message_uuid}: ${statusUpdate.status}`);
console.log(`Timestamp: ${statusUpdate.timestamp}`);
console.log(`Recipient: ${statusUpdate.to?.number}`);
if (statusUpdate.error) {
console.error(`Error Code: ${statusUpdate.error.code}`);
console.error(`Error Reason: ${statusUpdate.error.reason}`);
}
if (statusUpdate.usage) {
console.log(`Usage: Price ${statusUpdate.usage.price}${statusUpdate.usage.currency}`);
}
// --- Add your business logic here ---// Example: Update the status of the message in your database using message_uuid// updateMessageStatusInDB(statusUpdate.message_uuid, statusUpdate.status, statusUpdate.error, statusUpdate.timestamp);
} else {
console.warn('Received webhook does not appear to be a valid Messages API status update.');
}
// Vonage needs a 200 OK response.
res.status(200).end();
});
// --- End of Status Route ---// Start the server (keep existing)// ...
SMS Delivery Status Codes Explained:
Understanding these delivery status codes helps you track SMS delivery success:
Status
Description
Final State
submitted
Vonage accepted the message request
No
delivered
Recipient's carrier confirmed delivery to the handset
Yes
rejected
Vonage rejected before sending (e.g., invalid number, insufficient funds)
Yes
undeliverable
Carrier could not deliver (e.g., phone off, invalid number, blocked)
Yes
failed
General delivery failure
Yes
Note: You may receive multiple webhook callbacks per message as the status changes. Final states (delivered, rejected, undeliverable, failed) indicate no further status updates will arrive for that message.
Test Status Updates:
Ensure your server and ngrok are running.
Send an SMS using the /send-sms endpoint (Step 4). Note the message_uuid returned.
Watch your server console. You should receive one or more status updates for that message_uuid via the /webhooks/status endpoint.
Status updates typically arrive within seconds to minutes, depending on carrier processing times.
Check the ngrok web interface (http://127.0.0.1:4040) to inspect the incoming POST requests to /webhooks/status, including the JWT in the Authorization header.
7. Error Handling and Logging
Robust error handling and clear logging are essential for debugging and maintaining the application.
Error Handling:
API Calls: Wrap the vonage.messages.send call in a try...catch block. Log detailed error information from err.response.data if available, as it contains specific Vonage error details. Return appropriate HTTP status codes (e.g., 400 for bad input, 500 for server errors, Vonage status codes if meaningful).
Webhooks: Ensure webhook handlers always return a 200 OK status to Vonage quickly, even if internal processing fails. Log errors encountered during webhook processing for later debugging. Use try...catch within the webhook handlers for your business logic to prevent crashes from stopping the res.status(200).end() call. Handle asynchronous operations carefully.
Configuration: Check for the existence and validity of necessary environment variables (like VONAGE_NUMBER, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH) at startup. The vonageClient.js example includes validation that exits the process if the private key fails to load.
Logging:
Use console.log, console.warn, and console.error consistently for basic logging.
Log key events: server start, incoming API requests, outgoing API calls, successful operations, errors (with stack traces or detailed Vonage responses), and full webhook payloads/headers (especially during development).
Include unique identifiers like message_uuid in logs to trace the lifecycle of a message across sending, status updates, and potential inbound replies.
For production: Use a structured logging library like Winston or Pino. These libraries offer:
Different log levels (debug, info, warn, error, critical).
Structured formatting (JSON is common for log aggregation systems).
Multiple transports (writing to console, files, databases, external logging services like Datadog, Loggly, etc.).
JavaScript
// Example using Winston (requires npm install winston)const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // Control log level via env varformat: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }), // Log stack traces
winston.format.splat(),
winston.format.json() // Log as JSON
),
defaultMeta: { service: 'vonage-sms-app' }, // Add service name to logstransports: [
// Write errors to error.lognew winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error'
}),
// Write all logs (info and above) to combined.lognew winston.transports.File({
filename: path.join('logs', 'combined.log')
})
],
});
// If not in production, also log to the console with a simpler formatif (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// Usage example (replace console.log/error):// logger.info('Server started on port %d', PORT);// logger.error('Failed to send SMS:', err);// logger.warn('Webhook received with unexpected format:', req.body);// logger.debug('Detailed webhook body:', req.body); // Only logs if level is 'debug'
Best Practices:
Redact PII (personally identifiable information) like phone numbers and message content from production logs.
Use correlation IDs to trace requests across distributed systems.
Configure log rotation to prevent disk space issues.
Integrate with monitoring services (Sentry, New Relic) for real-time error tracking.
8. Security Considerations
Security is paramount when handling SMS and webhooks. Implement these measures for production:
Webhook JWT Verification (Critical):
Implement JWT signature verification for all webhook endpoints (/webhooks/inbound and /webhooks/status).
Extract the JWT from the Authorization: Bearer <token> header.
Verify the signature using Vonage's public key and the jsonwebtoken library.
Validate JWT claims including expiration (exp), issued-at (iat), and audience (aud).
Reject requests with invalid or expired tokens.
Refer to Vonage's official documentation for implementation details.
Private Key Security:
Never commit private.key or .pem files to version control. Add them to .gitignore.
In production, store private keys in secure secret management systems (AWS Secrets Manager, HashiCorp Vault, etc.).
Use environment variables or secure configuration management to inject keys at runtime.
Implement key rotation procedures and monitor for compromised keys.
Environment Variables:
Never hardcode credentials in source code.
Use different .env files for development, staging, and production environments.
Restrict access to environment variables in production systems.
Input Validation:
Validate all user inputs, especially phone numbers. Use libraries like libphonenumber-js for robust phone number validation.
Sanitize message content to prevent injection attacks.
Implement rate limiting on the /send-sms endpoint to prevent abuse (use libraries like express-rate-limit).
HTTPS/TLS:
Always use HTTPS for production webhooks. ngrok provides HTTPS, but production requires valid SSL/TLS certificates (Let's Encrypt, commercial CAs).
Ensure your server enforces TLS 1.2 or higher.
Error Messages:
Avoid exposing sensitive information in error messages returned to clients.
Log detailed errors server-side but return generic messages to API consumers.
9. Production Deployment
Deploying to production requires additional considerations beyond ngrok:
Infrastructure Requirements:
Stable Public URL: Use a cloud provider (AWS, Google Cloud, Azure, Heroku, DigitalOcean) or VPS with a static IP address or domain name.
SSL/TLS Certificate: Obtain a valid SSL certificate (free from Let's Encrypt or commercial CAs).
Process Manager: Use PM2, systemd, or Docker to keep your Node.js application running and restart on crashes.
Reverse Proxy: Use Nginx or Apache as a reverse proxy for SSL termination, load balancing, and security.
Conclusion: Building SMS Delivery Tracking Systems
You have successfully built a complete SMS delivery tracking application using Node.js, Express, and the Vonage Messages API with full webhook support:
Send SMS messages programmatically via API
Receive inbound SMS via webhook callbacks
Track SMS delivery status and delivery receipts (DLR) in real-time
This foundation enables you to build sophisticated SMS-based applications including two-factor authentication, notifications, customer support systems, and marketing campaigns.
Next Steps:
Implement JWT webhook verification for production security
Add database persistence for messages and status tracking
Implement rate limiting and input validation
Set up production deployment with monitoring and logging
Explore advanced Vonage features like MMS, WhatsApp, and conversation API
Frequently asked questions
Use the Vonage Messages API's vonage.messages.send() method. Provide the recipient's number, your Vonage virtual number, and the message text in the request body. Ensure your Vonage application is set up correctly with the necessary credentials and linked virtual number as described in the guide's setup steps.
The Vonage Messages API is a unified API for sending and receiving messages across various channels, including SMS. It offers comprehensive features for sending SMS programmatically, receiving inbound SMS to your virtual number, and tracking message delivery status through webhooks.
Using Application ID and Private Key with the Vonage Messages API enhances security by using JWT (JSON Web Tokens) for authentication. This approach is preferred over sending API Key/Secret with each request, as it reduces the risk of credential exposure.
Set up a webhook endpoint in your Node.js/Express application (e.g., /webhooks/inbound) and configure this URL in your Vonage Application settings. Vonage will send an HTTP POST request to this endpoint whenever an SMS is sent to your linked virtual number.
Run ngrok http <your-port> (e.g., ngrok http 3000) in a separate terminal. Copy the HTTPS forwarding URL provided by ngrok. Update your .env file's BASE_WEBHOOK_URL with this URL and configure your Vonage Application's inbound and status URLs using this base URL.
JWT verification ensures that incoming webhook requests genuinely originate from Vonage, preventing unauthorized access or malicious actors from spoofing webhook events. It is crucial for production security.
Configure a status webhook URL (e.g., /webhooks/status) in your Vonage application settings. Vonage will send POST requests to this endpoint with real-time delivery status updates (e.g., 'submitted', 'delivered', 'failed') for each message sent via the Messages API.
You'll need a Vonage API account, API Key and Secret, a purchased Vonage virtual number, Node.js and npm installed, ngrok for development, and a basic understanding of Node.js, Express, and APIs. The guide details how to acquire and configure each of these elements.
Implement JWT verification for all webhook endpoints to ensure requests are from Vonage. Regularly check and update your private key. Refer to Vonage's official documentation for the most accurate and up-to-date security recommendations.
The Messages API is Vonage's recommended approach for modern SMS integrations. It offers a unified API for various channels, including SMS, provides JWT-based webhook security, and has webhooks for inbound messages and delivery statuses. The SMS API is being superseded by the Messages API. For new projects, always use the Messages API.
The guide recommends E.164 format for phone numbers (e.g., +14155552671). While the Messages API may accept other formats, using E.164 promotes consistency and avoids potential issues.
Use try...catch blocks around API calls and within webhook handlers. Log detailed error information from err.response.data when available. Return appropriate HTTP status codes to indicate errors, distinguishing between bad user input (4xx errors) and server-side or Vonage issues (5xx errors).
This comprehensive guide shows you how to build a production-ready Node.js application using Express to send SMS messages, receive inbound SMS, and track delivery status in real-time using Vonage Messages API webhooks.
Learn how to implement SMS delivery tracking and webhooks in Node.js for:
This tutorial solves the common need for applications to reliably communicate with users via SMS and track message delivery confirmation. Perfect for building two-factor authentication (2FA), notification systems, and SMS marketing campaigns. Note: Test the code snippets rigorously in your environment, especially error handling and security implementations.
Technologies Used
@vonage/server-sdk: The official Vonage Node.js SDK (v3.x or later) for interacting with Vonage APIs.dotenv: A module to load environment variables from a.envfile.jsonwebtoken: (Conceptually needed for JWT verification, though implementation details are deferred to Vonage docs).System Architecture: How SMS Webhooks Work
The SMS delivery tracking system involves several components interacting:
Prerequisites
Before starting, ensure you have the following:
1. Setting up the Project
Initialize your Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-messages-guide cd vonage-messages-guideInitialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.(Use
yarn init -yif you prefer Yarn)Install Dependencies: Install
expressfor the web server,@vonage/server-sdkto interact with the Vonage API, anddotenvto manage environment variables. Also installjsonwebtokenfor webhook security.(Use
yarn add express @vonage/server-sdk dotenv jsonwebtokenfor Yarn)Create Project Structure: Create the necessary files and folders.
touch server.js .env .gitignore vonageClient.js mkdir logs # Optional: For file loggingserver.js: Main application file for the Express server and route handlers..env: Stores sensitive credentials and configuration (API keys, etc.). Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore (like.envandnode_modules).vonageClient.js: Module to initialize and export the Vonage SDK client.logs/: Directory for log files if using file transport for logging.Configure
.gitignore: Add the following lines to your.gitignorefile to prevent sensitive information and unnecessary files from being committed:Set up Environment Variables (
.env): Open the.envfile and add the following variables. Fill in the values in the next steps.VONAGE_API_KEY,VONAGE_API_SECRET: Found on your Vonage dashboard. Used by the SDK, though Application ID/Private Key is primary for Messages API.VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATH: Obtained when creating a Vonage Application (next section).VONAGE_NUMBER: Your purchased Vonage virtual phone number in E.164 format (e.g.,14155552671for US,447700900000for UK).PORT: The local port your Express application will listen on.BASE_WEBHOOK_URL: The public URL provided by ngrok (or your production server URL).Basic Express Server (
server.js): Set up a minimal Express server to ensure everything works.// server.js require('dotenv').config(); // Load .env variables into process.env const express = require('express'); const app = express(); // Middleware to parse JSON bodies // Vonage Messages API webhooks typically use JSON app.use(express.json()); // Include urlencoded parser for flexibility, though JSON is primary for Messages API app.use(express.urlencoded({ extended: true })); const PORT = process.env.PORT || 3000; // Simple root route for testing app.get('/', (req, res) => { res.send('Vonage Messages API Guide App is running!'); }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // Start the server app.listen(PORT, () => { console.log(`Server listening at http://localhost:${PORT}`); });Run this with
node server.js. You should see the confirmation message in your console. Accessinghttp://localhost:3000in your browser should show the test message.2. Vonage Account and Application Setup
Configure your Vonage account and create a Vonage Application to handle Messages API interactions.
Retrieve API Key and Secret:
.envfile forVONAGE_API_KEYandVONAGE_API_SECRET.Purchase a Vonage Number:
12015550123) into your.envfile forVONAGE_NUMBER.Set Default SMS API to "Messages API":
Create a Vonage Application: Vonage Applications act as containers for configuration, security credentials (like the private key), and associated numbers, specifically for APIs like Messages.
private.keyfile. Save this file securely in the root of your project directory (matchingVONAGE_PRIVATE_KEY_PATHin your.env). Crucially, addprivate.keyor*.pemto your.gitignorefile. Vonage stores the public key; you keep the private key to authenticate SDK requests using JWT.${BASE_WEBHOOK_URL}/webhooks/inbound(Method:POST)${BASE_WEBHOOK_URL}/webhooks/status(Method:POST).envfile forVONAGE_APPLICATION_ID.3. Setting up SMS Webhooks with ngrok (Development)
Webhooks enable real-time SMS delivery tracking by allowing Vonage to send instant notifications (inbound messages, delivery status updates) to your application. Since your application runs locally during development, use ngrok to create a secure tunnel from a public URL to your
localhost.Important: ngrok is excellent for development and testing, but not suitable for production. Production environments require a stable, publicly accessible IP address or domain name with a valid SSL/TLS certificate. See the Production Deployment section for more details.
Run ngrok: Open a new terminal window (keep your Node.js server running or be ready to restart it). Run ngrok, telling it to forward traffic to the port your Express app listens on (defined in
.env, default is 3000).Get the Forwarding URL: ngrok will display session information, including a public
ForwardingURL (usually ending in.ngrok.ioor.ngrok-free.app). It will look something likehttps://<random-string>.ngrok-free.app. Always use thehttpsversion.Copy this
https://...Forwarding URL. Note that free accounts generate a new random URL each time ngrok starts. Paid ngrok plans offer stable subdomains.Tip: Access the ngrok web interface at
http://127.0.0.1:4040to inspect all incoming requests, view headers (including JWT tokens), and debug webhook issues.Update
.env: Paste the copied ngrok Forwarding URL into your.envfile for theBASE_WEBHOOK_URLvariable.Update Vonage Application Webhooks:
https://<random-string>.ngrok-free.app/webhooks/inboundhttps://<random-string>.ngrok-free.app/webhooks/statusWhy? When Vonage needs to send an inbound message or a status update related to this application, it sends an HTTP POST request to these public ngrok URLs, which ngrok forwards to your locally running Express application's
/webhooks/inboundand/webhooks/statusroutes.Restart Your Node.js Server: If your server is running, stop it (
Ctrl+C) and restart it (node server.js) to ensure it picks up the updated.envconfiguration.4. Implementing SMS Sending
Create the Vonage SDK client and add an endpoint to send SMS messages using the Messages API.
Configure Vonage SDK Client (
vonageClient.js): This module initializes the Vonage client using credentials from the.envfile. Using Application ID and Private Key is the recommended authentication method for the Messages API as it uses JWTs.// vonageClient.js require('dotenv').config(); const { Vonage } = require('@vonage/server-sdk'); const { readFileSync } = require('fs'); // To read the private key file // Read the private key from the file path specified in .env let privateKeyValue; try { privateKeyValue = readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH); } catch (error) { console.error(`Error reading private key from path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`, error); process.exit(1); // Exit if private key cannot be loaded } const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, // Still required by SDK constructor apiSecret: process.env.VONAGE_API_SECRET, // Still required by SDK constructor applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyValue // Use the key content read from the file }, { // Optional: Set custom options here // apiHost: 'https://messages-sandbox.nexmo.com' // Example: Use the sandbox }); module.exports = vonage;Why use Application ID/Private Key? This method uses JWTs generated by the SDK for authenticating API requests, which is more secure than sending API Key/Secret with each request for server-to-server communication via the Messages API.
Create Send SMS Endpoint (
server.js): Add a new route to yourserver.jsfile to handle sending SMS messages. Make it aPOSTrequest that accepts the recipient number and message text in the request body.// server.js (add these parts) const vonage = require('./vonageClient'); // Import the initialized client // Keep existing setup: require('dotenv').config(), express, app, middleware... app.get('/', (req, res) => { // Keep existing root route res.send('Vonage Messages API Guide App is running!'); }); app.get('/health', (req, res) => { // Keep health check route res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // --- New Route for Sending SMS --- app.post('/send-sms', async (req, res) => { // Basic input validation const { to, text } = req.body; if (!to || !text) { return res.status(400).json({ error: 'Missing "to" or "text" in request body.' }); } // Consider adding phone number validation using libphonenumber-js in production const fromNumber = process.env.VONAGE_NUMBER; if (!fromNumber) { console.error('VONAGE_NUMBER is not set in .env'); return res.status(500).json({ error: 'Server configuration error: Missing Vonage number.' }); } console.log(`Attempting to send SMS via Messages API from ${fromNumber} to ${to}`); try { // Use the vonage.messages.send() method for the Messages API const resp = await vonage.messages.send({ channel: 'sms', message_type: 'text', to: to, // Recipient phone number (E.164 format recommended) from: fromNumber, // Your Vonage virtual number linked to the Application text: text // The message content (max 160 chars for single SMS) }); console.log('Message submission accepted by Vonage:', resp); // The resp object contains the message_uuid res.status(200).json({ success: true, message_uuid: resp.message_uuid }); } catch (err) { console.error('Error sending SMS via Messages API:', err.response ? JSON.stringify(err.response.data, null, 2) : err.message); // Provide more context if available from the Vonage error response const errorMessage = err.response?.data?.title || err.message || 'Failed to send SMS'; const errorDetail = err.response?.data?.detail || JSON.stringify(err.response?.data); // Include full detail if possible res.status(err.response?.status || 500).json({ success: false, error: errorMessage, detail: errorDetail }); } }); // --- End of New Route --- // ... (Add webhook routes later) // Start the server (keep existing) const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server listening at http://localhost:${PORT}`); });Note: SMS messages longer than 160 characters automatically concatenate into multiple segments. Each segment is billed separately. Consider truncating or paginating long messages.
Test Sending SMS: Make sure your Node.js server and ngrok are running. Use
curlor a tool like Postman/Insomnia to send a POST request to your/send-smsendpoint. Replace<your-ngrok-url>with your actual ngrok Forwarding URL and<recipient-phone-number>with a real phone number (in E.164 format, e.g.,14155552671).curl -X POST <your-ngrok-url>/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "<recipient-phone-number>", "text": "Hello from Vonage Messages API and Node.js!" }'You should receive an SMS on the recipient phone shortly. Your server console will log the success response from Vonage, including the
message_uuid, which is crucial for tracking. The API call should return a JSON response like{"success":true,"message_uuid":"<some-uuid>"}. If errors occur, check the console logs for details from thecatchblock.Common Errors:
.env.5. Implementing Inbound SMS Webhook (Receive SMS)
This webhook endpoint handles incoming SMS messages sent to your Vonage virtual number. When someone texts your number, Vonage forwards the message to your webhook URL via HTTP POST request.
Create Inbound Webhook Route (
server.js): Add aPOSTroute handler for the/webhooks/inboundpath you configured in your Vonage Application.// server.js (add this route) // ... (existing code: imports, middleware, /send-sms route) // --- New Route for Inbound SMS --- app.post('/webhooks/inbound', (req, res) => { console.log('--- Inbound SMS Webhook Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log headers for debugging signature verification later console.log('Request Headers:', JSON.stringify(req.headers, null, 2)); // --- Security Check Placeholder --- // IMPORTANT: Implement JWT verification here for production! // See Security Considerations section and Vonage documentation. // verifyVonageSignature(req); // This function needs proper implementation // Process the inbound message (assuming signature verification passed or is deferred) const message = req.body; // Check structure for typical Messages API inbound SMS if (message.from?.type === 'sms' && message.message?.content?.type === 'text') { console.log(`From: ${message.from.number}`); console.log(`To: ${message.to.number}`); // Your Vonage number console.log(`Text: ${message.message.content.text}`); console.log(`Message UUID: ${message.message_uuid}`); console.log(`Timestamp: ${message.timestamp}`); // --- Add your business logic here --- // Example: Store the message, trigger a reply, etc. // e.g., replyToSms(message.from.number, 'Thanks for your message!'); } else { console.warn('Received webhook does not appear to be a standard inbound SMS via Messages API.'); } // Vonage needs a 200 OK response quickly to acknowledge receipt. // Do complex processing asynchronously if needed. res.status(200).end(); }); // --- End of Inbound Route --- // ... (Add status webhook route next) // Start the server (keep existing) // ...Understanding Webhook Signature Verification (JWT): The Messages API secures webhooks using JWT (JSON Web Tokens) attached to the
Authorizationheader (typicallyBearer <token>). Verifying this signature is critical for production security to ensure requests genuinely come from Vonage.JWT Verification Steps:
Authorization: Bearer <token>headerjsonwebtoken.verify(token, publicKey, { algorithms: ['RS256'] })iat,exp,aud)Refer to the official Vonage Developer Documentation on "Signed Webhooks" or "Webhook Security" specifically for the Messages API for the authoritative and up-to-date implementation guide.
Test Inbound SMS:
VONAGE_NUMBER).http://127.0.0.1:4040) to inspect the incoming POST request to/webhooks/inbound, including theAuthorizationheader containing the JWT.6. Implementing Delivery Status Webhook (Track SMS Delivery)
This webhook receives real-time delivery receipt (DLR) updates about the delivery status of outbound messages you sent using the Messages API. Track whether your SMS was delivered, failed, or rejected by the carrier.
server.js): Add aPOSTroute handler for the/webhooks/statuspath.// server.js (add this route) // ... (existing code: imports, middleware, /send-sms, /webhooks/inbound) // --- New Route for Delivery Status Updates --- app.post('/webhooks/status', (req, res) => { console.log('--- Delivery Status Webhook Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log headers for debugging signature verification later console.log('Request Headers:', JSON.stringify(req.headers, null, 2)); // --- Security Check Placeholder --- // IMPORTANT: Implement JWT verification here for production! // Use the same logic/mechanism as for the inbound webhook. // See Security Considerations section and Vonage documentation. // verifyVonageSignature(req); // This function needs proper implementation // Process the status update (assuming signature verification passed or is deferred) const statusUpdate = req.body; // Key fields in a Messages API status update if (statusUpdate.message_uuid && statusUpdate.status) { console.log(`Status for message ${statusUpdate.message_uuid}: ${statusUpdate.status}`); console.log(`Timestamp: ${statusUpdate.timestamp}`); console.log(`Recipient: ${statusUpdate.to?.number}`); if (statusUpdate.error) { console.error(`Error Code: ${statusUpdate.error.code}`); console.error(`Error Reason: ${statusUpdate.error.reason}`); } if (statusUpdate.usage) { console.log(`Usage: Price ${statusUpdate.usage.price} ${statusUpdate.usage.currency}`); } // --- Add your business logic here --- // Example: Update the status of the message in your database using message_uuid // updateMessageStatusInDB(statusUpdate.message_uuid, statusUpdate.status, statusUpdate.error, statusUpdate.timestamp); } else { console.warn('Received webhook does not appear to be a valid Messages API status update.'); } // Vonage needs a 200 OK response. res.status(200).end(); }); // --- End of Status Route --- // Start the server (keep existing) // ...SMS Delivery Status Codes Explained:
Understanding these delivery status codes helps you track SMS delivery success:
submitteddeliveredrejectedundeliverablefailedNote: You may receive multiple webhook callbacks per message as the status changes. Final states (
delivered,rejected,undeliverable,failed) indicate no further status updates will arrive for that message./send-smsendpoint (Step 4). Note themessage_uuidreturned.message_uuidvia the/webhooks/statusendpoint.http://127.0.0.1:4040) to inspect the incoming POST requests to/webhooks/status, including the JWT in theAuthorizationheader.7. Error Handling and Logging
Robust error handling and clear logging are essential for debugging and maintaining the application.
Error Handling:
vonage.messages.sendcall in atry...catchblock. Log detailed error information fromerr.response.dataif available, as it contains specific Vonage error details. Return appropriate HTTP status codes (e.g., 400 for bad input, 500 for server errors, Vonage status codes if meaningful).200 OKstatus to Vonage quickly, even if internal processing fails. Log errors encountered during webhook processing for later debugging. Usetry...catchwithin the webhook handlers for your business logic to prevent crashes from stopping theres.status(200).end()call. Handle asynchronous operations carefully.VONAGE_NUMBER,VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATH) at startup. ThevonageClient.jsexample includes validation that exits the process if the private key fails to load.Logging:
console.log,console.warn, andconsole.errorconsistently for basic logging.message_uuidin logs to trace the lifecycle of a message across sending, status updates, and potential inbound replies.// Example using Winston (requires npm install winston) const winston = require('winston'); const path = require('path'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Control log level via env var format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Log stack traces winston.format.splat(), winston.format.json() // Log as JSON ), defaultMeta: { service: 'vonage-sms-app' }, // Add service name to logs transports: [ // Write errors to error.log new winston.transports.File({ filename: path.join('logs', 'error.log'), level: 'error' }), // Write all logs (info and above) to combined.log new winston.transports.File({ filename: path.join('logs', 'combined.log') }) ], }); // If not in production, also log to the console with a simpler format if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) })); } // Usage example (replace console.log/error): // logger.info('Server started on port %d', PORT); // logger.error('Failed to send SMS:', err); // logger.warn('Webhook received with unexpected format:', req.body); // logger.debug('Detailed webhook body:', req.body); // Only logs if level is 'debug'Best Practices:
8. Security Considerations
Security is paramount when handling SMS and webhooks. Implement these measures for production:
Webhook JWT Verification (Critical):
/webhooks/inboundand/webhooks/status).Authorization: Bearer <token>header.jsonwebtokenlibrary.exp), issued-at (iat), and audience (aud).Private Key Security:
private.keyor.pemfiles to version control. Add them to.gitignore.Environment Variables:
.envfiles for development, staging, and production environments.Input Validation:
libphonenumber-jsfor robust phone number validation./send-smsendpoint to prevent abuse (use libraries likeexpress-rate-limit).HTTPS/TLS:
Error Messages:
9. Production Deployment
Deploying to production requires additional considerations beyond ngrok:
Infrastructure Requirements:
Production Checklist:
https://yourdomain.com/webhooks/inbound).Scaling Considerations:
10. Additional Resources
Conclusion: Building SMS Delivery Tracking Systems
You have successfully built a complete SMS delivery tracking application using Node.js, Express, and the Vonage Messages API with full webhook support:
This foundation enables you to build sophisticated SMS-based applications including two-factor authentication, notifications, customer support systems, and marketing campaigns.
Next Steps: