Vonage SMS Delivery Status and Callbacks with Node.js, Next.js, and Supabase
Learn how to implement SMS delivery status tracking and webhook callbacks using the Vonage Messages API with Node.js. This comprehensive guide covers sending SMS messages, receiving delivery receipts (DLR), and handling inbound message webhooks for real-time SMS communication tracking.
Learn how to implement SMS delivery status tracking and webhook callbacks using the Vonage Messages API with Node.js. This comprehensive guide covers sending SMS messages, receiving delivery receipts (DLR), and handling inbound message webhooks for real-time SMS communication tracking.
What You'll Build
By the end of this tutorial, you will have a production-ready Node.js application that:
Sends SMS messages programmatically using the Vonage Messages API
Receives real-time delivery status updates (submitted, delivered, failed, rejected)
Handles inbound SMS messages sent to your Vonage virtual number
Implements webhook endpoints for reliable two-way SMS communication
This implementation is essential for applications requiring SMS delivery confirmation, including:
Two-factor authentication (2FA) systems that need delivery verification
Understanding Vonage SMS Delivery Status and Webhooks
When you send an SMS message through the Vonage Messages API, you receive an immediate HTTP response confirming the message was submitted to the carrier network. However, this doesn't guarantee the message reached the recipient's device. To track actual delivery, you need to implement delivery receipt (DLR) webhooks that receive real-time status updates as your message progresses through the carrier network.
This tutorial builds a robust Node.js backend that integrates Vonage's webhook system for complete SMS delivery tracking and two-way messaging capabilities.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications.
Express: A minimal and flexible Node.js web application framework used to create our webhook endpoints.
Vonage Messages API: A unified API for sending and receiving messages across various channels (SMS, MMS, WhatsApp, etc.). We will focus on SMS.
@vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.
dotenv: A module to load environment variables from a .env file for secure configuration management.
ngrok: A tool to expose local development servers to the internet, necessary for testing Vonage webhooks.
server.js: Runs the Express server to listen for webhooks.
send-sms.js: Contains the logic to send an SMS message.
.gitignore: Specifies files/directories that Git should ignore (like .env and node_modules).
5. Configure .gitignore:
Add the following lines to your .gitignore file to prevent committing sensitive information and unnecessary files:
Text
node_modules
.env
private.key
*.log
6. Set Up Environment Variables (.env):
Open the .env file and prepare it for your Vonage credentials. We'll fill these in later.
.env
# .env - Fill these values in later
# Vonage API Credentials (Found in Vonage Dashboard -> API Settings)
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
# Vonage Application ID (Generated in Step 2)
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
# Path to your Vonage Application private key file (Downloaded in Step 2)
VONAGE_PRIVATE_KEY_PATH=./private.key
# Your Vonage virtual number (e.g., 14155550100)
VONAGE_NUMBER=YOUR_VONAGE_NUMBER
# Recipient phone number for testing (e.g., 14155550101)
TO_NUMBER=RECIPIENT_PHONE_NUMBER
# Port for the local Express server
PORT=3000
Why .env? Storing credentials directly in code is insecure. Using environment variables loaded from .env (which is gitignored) keeps secrets out of your source control. dotenv makes this easy in development. In production, you'd typically set these variables directly in your hosting environment.
Configuring Vonage Webhooks for SMS Delivery Status
To receive delivery status callbacks and inbound messages, you must configure webhook URLs in your Vonage Application. Vonage will send HTTP POST requests to these endpoints whenever message events occur (delivery status changes, incoming messages).
Ensure the Default SMS Setting is set to Messages API. If not, select it and click Save changes.
Why? Vonage has two SMS APIs (SMS API and Messages API). They use different webhook formats and configurations. This guide uses the newer Messages API, so setting it as the default ensures consistency.
2. Run ngrok:
Before creating the Vonage Application, you need a publicly accessible URL for your local server's webhooks. ngrok provides this. Open a new terminal window and run:
Bash
ngrok http 3000
(Replace 3000 if you chose a different port in .env)
ngrok will display output similar to this:
Text
Session Status online
Account Your Name (Plan: Free)
Version x.x.x
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding https://<random-string>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https://<random-string>.ngrok-free.app URL. This is your public base URL. Keep this terminal window running.
3. Create a Vonage Application:
In the Vonage Dashboard, navigate to Applications > Create a new application.
Enter an Application name (e.g., Node SMS Callbacks App).
Click Generate public and private key. This will automatically download a file named private.key. Save this file in your project's root directory (the same place as server.js). This key is used by the SDK to authenticate requests for this application.
Under Capabilities, find Messages and toggle it ON.
Two fields will appear: Inbound URL and Status URL.
Inbound URL: Paste your ngrok Forwarding URL and append /webhooks/inbound. Example: https://<random-string>.ngrok-free.app/webhooks/inbound
Status URL: Paste your ngrok Forwarding URL and append /webhooks/status. Example: https://<random-string>.ngrok-free.app/webhooks/status
Why these URLs? The Inbound URL is where Vonage sends data when your Vonage number receives an SMS. The Status URL is where Vonage sends delivery receipt updates for messages you send.
Leave other capabilities off for now.
Click Generate new application.
You'll be taken to the application's page. Copy the Application ID displayed near the top.
4. Link Your Vonage Number:
On the application's page (or by going back to Applications and clicking your app name), scroll down to the Linked numbers section.
Click Link next to the Vonage virtual number you want to use for sending and receiving SMS with this application.
Confirm the linking.
5. Update .env File:
Now, open your .env file and fill in the values you obtained:
.env
# .env - Update with your actual values
VONAGE_API_KEY=YOUR_VONAGE_API_KEY # From Dashboard API Settings
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # From Dashboard API Settings
VONAGE_APPLICATION_ID=YOUR_COPIED_APPLICATION_ID # From Step 3
VONAGE_PRIVATE_KEY_PATH=./private.key # Should be correct if saved in root
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # e.g., 14155550100
TO_NUMBER=YOUR_TEST_RECIPIENT_PHONE_NUMBER # e.g., 14155550101
PORT=3000
Ensure VONAGE_PRIVATE_KEY_PATH correctly points to where you saved the private.key file.
Sending SMS Messages with Node.js and Tracking Message UUIDs
Every SMS sent through the Vonage Messages API receives a unique message_uuid identifier. This UUID is critical for tracking delivery status updates, as Vonage includes it in webhook callbacks to match status updates with the original message.
File:send-sms.js
JavaScript
// send-sms.jsrequire('dotenv').config(); // Load environment variables from .env fileconst fs = require('fs'); // Require Node.js file system module to read the private keyconst { Vonage } = require('@vonage/server-sdk');
// Note: Messages capability is accessed via the main Vonage instance// --- Configuration ---const vonageApiKey = process.env.VONAGE_API_KEY;
const vonageApiSecret = process.env.VONAGE_API_SECRET;
const vonageApplicationId = process.env.VONAGE_APPLICATION_ID;
const vonagePrivateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH;
const vonageNumber = process.env.VONAGE_NUMBER;
const toNumber = process.env.TO_NUMBER;
// --- Input Validation (Basic) ---if (!vonageApiKey || !vonageApiSecret || !vonageApplicationId || !vonagePrivateKeyPath || !vonageNumber || !toNumber) {
console.error('Error: Missing required environment variables. Check your .env file.');
process.exit(1); // Exit if configuration is incomplete
}
let privateKey;
try {
privateKey = fs.readFileSync(vonagePrivateKeyPath);
} catch (err) {
console.error(`Error reading private key file at path: ${vonagePrivateKeyPath}`);
console.error(err.message);
process.exit(1); // Exit if private key cannot be read
}
// --- Initialize Vonage Client ---// Note: For Messages API with Application ID and Private Key,// you initialize the main Vonage client slightly differently.// The API Key and Secret are used for authentication along with the Application ID and Private Key.const vonage = newVonage({
apiKey: vonageApiKey,
apiSecret: vonageApiSecret, // Secret is still needed for some auth mechanisms, like signed webhooksapplicationId: vonageApplicationId,
privateKey: privateKey
});
// --- Define the send SMS function ---asyncfunctionsendSms() {
console.log(`Attempting to send SMS from ${vonageNumber} to ${toNumber}...`);
try {
// Use vonage.messages.send() for the Messages APIconst resp = await vonage.messages.send({
message_type: "text",
text: "Hello from Vonage and Node.js!",
to: toNumber,
from: vonageNumber,
channel: "sms"
});
console.log('SMS Sent Successfully!');
console.log('Message UUID:', resp.messageUuid); // Save this UUID for tracking delivery status
} catch (err) {
console.error('Error sending SMS:');
if (err.response?.data) {
// Log detailed Vonage API error if availableconsole.error('Vonage API Error:', JSON.stringify(err.response.data, null, 2));
} else {
// Log the general error objectconsole.error(err);
}
}
}
// --- Execute the function ---sendSms();
Explanation:
Load Env Vars & FS: Loads .env variables and the Node.js fs module.
Configuration: Reads all necessary variables from process.env.
Basic Validation: Checks if required environment variables are set.
Read Private Key: Reads the private key file content using fs.readFileSync. Includes error handling if the file is missing or unreadable.
Initialize Vonage Client: Creates a Vonage instance, passing the API key, secret, application ID, and the content of the private key.
sendSms Function:
Defines an async function to handle the asynchronous API call.
Uses vonage.messages.send() which is the correct method for the Messages API.
Constructs the message payload specifying message_type, text, to, from, and channel.
Uses a try...catch block for error handling.
Logs the messageUuid from the successful response (resp.messageUuid) - this UUID is essential for correlating delivery status updates.
Logs detailed error information if the API call fails.
Execute: Calls the sendSms function to initiate the process.
Building Express Webhook Endpoints for SMS Delivery Status
Create Express.js endpoints to receive Vonage webhook callbacks. You'll need two separate endpoints: one for delivery status updates (/webhooks/status) and one for inbound messages (/webhooks/inbound).
File:server.js
JavaScript
// server.jsrequire('dotenv').config();
const express = require('express');
const app = express();
// Vonage needs to POST JSON data to the webhooks
app.use(express.json());
// Sometimes data might be URL-encoded (less common for Messages API webhooks)
app.use(express.urlencoded({ extended: true }));
constPORT = process.env.PORT || 3000; // Use port from .env or default to 3000// --- Webhook Endpoints ---// Endpoint for Inbound SMS Messages
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound SMS Received ---');
console.log('From:', req.body.from?.number || req.body.from); // Handle potential structure variationsconsole.log('To:', req.body.to?.number || req.body.to);
console.log('Message:', req.body.message?.content?.text || req.body.text); // Handle potential structure variationsconsole.log('Full Body:', JSON.stringify(req.body, null, 2));
console.log('--------------------------\n');
// Vonage requires a 200 OK response to acknowledge receipt of the webhook
res.status(200).end();
});
// Endpoint for Delivery Status Updates
app.post('/webhooks/status', (req, res) => {
console.log('--- Delivery Status Update ---');
console.log('Message UUID:', req.body.message_uuid);
console.log('Status:', req.body.status);
console.log('Timestamp:', req.body.timestamp);
if (req.body.error) {
console.error('Error Code:', req.body.error.code);
console.error('Error Reason:', req.body.error.reason);
}
console.log('Full Body:', JSON.stringify(req.body, null, 2));
console.log('--------------------------\n');
// Acknowledge receipt
res.status(200).end();
});
// --- Server Start ---
app.listen(PORT, () => {
console.log(`Server listening for webhooks at http://localhost:${PORT}`);
console.log('Ensure ngrok is running and configured in the Vonage Dashboard:');
console.log('Inbound URL: /webhooks/inbound');
console.log('Status URL: /webhooks/status');
});
Explanation:
Load Env Vars:require('dotenv').config(); loads the variables from .env.
Initialize Express:const app = express(); creates the Express application.
Middleware:
app.use(express.json());: Parses incoming requests with JSON payloads (which Vonage uses for webhooks).
app.use(express.urlencoded({ extended: true }));: Parses incoming requests with URL-encoded payloads.
Webhook Endpoints:
app.post('/webhooks/inbound', ...): Defines a handler for POST requests to the /webhooks/inbound path. It logs relevant information from the request body (req.body).
app.post('/webhooks/status', ...): Defines a handler for POST requests to the /webhooks/status path. It logs the delivery status information including the message_uuid to correlate with sent messages.
res.status(200).end();: This is critical. Vonage expects a 200 OK response quickly to acknowledge receipt. Failure to send this promptly will cause Vonage to retry the webhook.
Start Server:app.listen(PORT, ...) starts the server on the specified port.
Implementing Proper Error Handling and Logging
The current code includes basic console.log statements. For production, you'd want more robust logging and error handling.
Error Handling (send-sms.js):
The try...catch block in the updated send-sms.js already handles errors during the API call more robustly. You can enhance this further:
JavaScript
// Example enhancement in send-sms.js catch block (already improved in Section 3)// ... inside the catch block ...console.error("Error sending SMS:");
// Log specific Vonage error details if availableif (err.response?.data) {
console.error("Vonage Error:", JSON.stringify(err.response.data, null, 2));
} else {
console.error(err); // Log the generic error
}
// Implement retry logic here if needed (e.g., for network errors)// Send error notification (e.g., to Sentry, Datadog)// ...
Logging (server.js):
Replace console.log with a dedicated logging library like winston or pino for structured logging, different log levels (info, warn, error), and log rotation.
Bash
npm install winston --save
JavaScript
// Example using Winston in server.js (simplified)const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
// Add file transport for production// new winston.transports.File({ filename: 'error.log', level: 'error' }),// new winston.transports.File({ filename: 'combined.log' }),
],
});
// Replace console.log with logger.info, logger.warn, logger.error// e.g., inside /webhooks/inbound// logger.info('--- Inbound SMS Received ---');// logger.info(`From: ${req.body.from?.number || req.body.from}`);// ...etc.// In case of webhook processing error:// try { /* process webhook */ } catch (error) {// logger.error('Error processing inbound webhook:', error);// res.status(500).end(); // Respond with an error, but be cautious as Vonage might retry// }
app.listen(PORT, () => {
logger.info(`Server listening for webhooks at http://localhost:${PORT}`);
// ...
});
Retry Mechanisms (Vonage):
Webhook Retries: Vonage automatically retries webhook delivery if it doesn't receive a 200 OK. Ensure your webhook endpoints respond quickly. Offload time-consuming processing.
Sending Retries: For sending SMS, implement your own retry logic within the .catch block in send-sms.js if necessary (e.g., for transient network errors), potentially using exponential backoff.
Database Schema and Data Layer (Conceptual)
This guide focuses on the core integration. In a real application, you would store message details and status updates in a database.
Conceptual Schema (e.g., PostgreSQL):
SQL
CREATE TABLE sms_messages (
message_uuid UUID PRIMARY KEY, -- Vonage Message UUID
vonage_number VARCHAR(20) NOT NULL,
recipient_number VARCHAR(20) NOT NULL,
message_text TEXT,
direction VARCHAR(10) NOT NULL, -- 'outbound' or 'inbound'
status VARCHAR(20) DEFAULT'submitted', -- e.g., submitted, delivered, failed, rejected
vonage_status_timestamp TIMESTAMPTZ, -- Timestamp from status webhook
error_code VARCHAR(10),
error_reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for querying by status or recipientCREATE INDEX idx_sms_messages_status ON sms_messages(status);
CREATE INDEX idx_sms_messages_recipient ON sms_messages(recipient_number);
Data Layer Implementation:
Use an ORM (like Prisma, Sequelize, TypeORM) or a query builder (like Knex.js).
Sending: After vonage.messages.send() succeeds, insert a record with direction='outbound', status='submitted', and the message_uuid.
Status Webhook: Find the message by message_uuid and update its status, vonage_status_timestamp, etc.
Inbound Webhook: Insert a new record with direction='inbound', status='delivered', and details from the payload.
Adding Security Features
Secure Credential Management: Handled via .env and .gitignore. Use environment variables or secrets management in production. Never commit secrets.
Webhook Signature Verification (Recommended for Production): Vonage signs Messages API webhook requests using HMAC-SHA256 with your API Secret, allowing verification.
Look for the X-Vonage-Signature header in incoming webhook requests.
You'll need to generate the signature yourself using the request body and your VONAGE_API_SECRET and compare it to the header value.
This prevents attackers from sending fake webhooks to your endpoints.
Consult the Vonage Webhook Signature Documentation for the precise steps to implement signed webhook verification for the Messages API. The @vonage/server-sdk may offer utilities, but manual implementation might be needed. Implementing this is crucial for production security.
Input Validation: Validate any user input if you build layers on top of this service.
Rate Limiting: Apply rate limiting to public-facing endpoints, generally not needed for Vonage webhooks themselves.
HTTPS:ngrok provides HTTPS. Ensure your production deployment uses HTTPS for webhook URLs.
Understanding SMS Delivery Status Values
Vonage delivery status webhooks include a status field indicating the current message state:
submitted: Message accepted by Vonage and sent to the carrier network
delivered: Carrier confirmed the message reached the recipient's device
rejected: Message rejected by carrier (often due to content filtering)
expired: Message expired before delivery (recipient phone off for extended period)
Important: Not all carriers support delivery receipts. In some countries/networks, you may only receive submitted status. Always handle cases where delivered status never arrives.
Additional Implementation Considerations
Character Encoding: The Messages API handles Unicode characters and emojis correctly with message_type: 'text'
Multipart Messages: Long SMS messages (over 160 characters) are automatically split into segments. You may receive separate delivery receipts for each segment
Phone Number Formatting: Always use E.164 format (e.g., +14155550101) for international compatibility
Rate Limiting: Implement request queuing for bulk SMS sending to respect Vonage API rate limits
Implementing Performance Optimizations
Fast Webhook Responses: Respond 200 OK immediately. Offload slow processing (DB writes, external calls) to background jobs (e.g., bullmq, agenda, AWS SQS).
Database Indexing: Index database tables (like sms_messages) on frequently queried columns (message_uuid, status, numbers).
Resource Usage: Monitor Node.js memory/CPU. Use tools like pm2 for process management and clustering.
Adding Monitoring, Observability, and Analytics
Health Checks: Add a /health endpoint returning 200 OK.
Structured Logging: Use Winston/Pino with JSON format for log aggregation (Datadog, Splunk, ELK).
Error Tracking: Integrate Sentry, Bugsnag, etc., to capture unhandled exceptions.
Metrics: Track SMS sent/failed, inbound received, webhook response times, status counts. Use Prometheus/Grafana, Datadog APM.
Dashboards: Visualize key metrics for system health and activity.
Troubleshooting Common Issues
Incorrect Credentials/IDs: Double-check VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH in .env. Ensure the private key file exists and is readable.
ngrok Issues:
Ensure ngrok runs and points to the correct local port (3000).
Verify the ngrok Forwarding URL in the Vonage Application webhook settings is current (it changes on restart for free accounts).
Check the ngrok web interface (http://127.0.0.1:4040) for requests/errors.
Firewall: Ensure your local firewall allows incoming connections on your server port (3000).
Webhook Not Returning 200 OK: Check server logs (server.js output) for errors if Vonage retries webhooks. Ensure quick responses.
Messages API Not Default: Re-verify Messages API is the default SMS setting in the Vonage Dashboard (API Settings) if encountering unexpected issues.
Delivery Receipt (DLR) Limitations: DLRs aren't guaranteed by all carriers. submitted confirms handoff to the carrier.
Invalid private.key: Ensure the private.key file matches the VONAGE_APPLICATION_ID and its content is correct.
Number Formatting Errors: Use E.164 format (+1...) for phone numbers.
Deployment and CI/CD
Beyond ngrok: Deploy server.js to a hosting provider (Heroku, AWS, Google Cloud, etc.).
Environment Variables: Configure production environment variables securely via your host. Do not deploy the .env file. Upload private.key securely or store its content in an environment variable (handle newlines carefully).
Process Management: Use pm2 to run your Node.js app:
Use the Vonage Messages API with the @vonage/server-sdk in your Node.js application. The send-sms.js example demonstrates how to initialize the Vonage client and use the vonage.messages.send() method to send text messages. Ensure your Vonage application is set up correctly with the necessary API credentials.
A Vonage Application acts as a container that connects your Vonage API keys, private key, virtual numbers, and webhook URLs. It's essential for sending SMS messages and receiving inbound messages and delivery receipts. Create one through the Vonage API Dashboard.
First, run ngrok to create a public URL for your local server. Then, in your Vonage Application settings, set the Inbound URL to your ngrok URL + /webhooks/inbound and the Status URL to your ngrok URL + /webhooks/status. This tells Vonage where to send incoming messages and delivery updates.
A 200 OK response from your webhook endpoint acknowledges to Vonage that you've successfully received the webhook. If Vonage doesn't receive this acknowledgment, it will retry sending the webhook multiple times to avoid data loss.
The private.key file is crucial for authenticating your application with Vonage. It is generated when you create a Vonage Application and should be kept secure, never committed to version control.
Set up a webhook endpoint (e.g., /webhooks/status) in your Express server. Vonage will send POST requests to this URL with delivery status updates, including 'submitted', 'delivered', 'failed', or 'rejected'. Your endpoint should parse this data and update your application's internal state accordingly.
Configure the /webhooks/inbound endpoint on your server and link it to your Vonage Application and virtual number. When someone sends an SMS to your Vonage number, Vonage will send the message data to your inbound webhook URL.
The Messages API is Vonage's recommended API for sending and receiving SMS messages. It's more versatile than the older SMS API and offers richer features, including unified handling of different message types.
The delivery receipt webhook includes the message_uuid, status (e.g., delivered, failed), timestamp, and any error codes/reasons if the delivery failed. Log this information for debugging and monitoring.
Yes, it is highly recommended to use environment variables (.env file for development, system environment variables for production) to store sensitive credentials like API keys and secrets. This prevents accidental exposure in your codebase.
Implement a try...catch block around the vonage.messages.send() call to handle potential errors during the API request. Log detailed error information, including any Vonage-specific error responses, and implement retry logic if necessary.
ngrok provides a public URL that tunnels to your local development server. This is necessary for Vonage to send webhook requests to your local machine during development and testing.
Use a structured logging library like Winston or Pino for better organization, different log levels (info, error, warning), and easier log management in a production environment.
Start your Express server and ngrok. Run the send-sms.js script to send a test SMS and observe the logs in your server console and the ngrok web interface for delivery status updates and webhook requests. Also, send an SMS to your Vonage number to test the inbound webhook.
Learn how to implement SMS delivery status tracking and webhook callbacks using the Vonage Messages API with Node.js. This comprehensive guide covers sending SMS messages, receiving delivery receipts (DLR), and handling inbound message webhooks for real-time SMS communication tracking.
What You'll Build
By the end of this tutorial, you will have a production-ready Node.js application that:
This implementation is essential for applications requiring SMS delivery confirmation, including:
Understanding Vonage SMS Delivery Status and Webhooks
When you send an SMS message through the Vonage Messages API, you receive an immediate HTTP response confirming the message was submitted to the carrier network. However, this doesn't guarantee the message reached the recipient's device. To track actual delivery, you need to implement delivery receipt (DLR) webhooks that receive real-time status updates as your message progresses through the carrier network.
This tutorial builds a robust Node.js backend that integrates Vonage's webhook system for complete SMS delivery tracking and two-way messaging capabilities.
Technologies Used:
@vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.dotenv: A module to load environment variables from a.envfile for secure configuration management.ngrok: A tool to expose local development servers to the internet, necessary for testing Vonage webhooks.System Architecture:
Prerequisites:
ngrokinstalled and authenticated (Download ngrok). A free account is sufficient.Final Outcome:
You will have two main components:
send-sms.js) to send an SMS message via Vonage and capture the unique message UUID for tracking.server.js) with webhook endpoints for receiving delivery status updates and inbound messages from Vonage.Setting Up Your Node.js Project for Vonage SMS Webhooks
Let's create the project structure and install the necessary dependencies for handling Vonage SMS delivery status callbacks.
1. Create Project Directory:
Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-callbacks cd vonage-sms-callbacks2. Initialize Node.js Project:
Initialize a
package.jsonfile.(This accepts default settings. Feel free to omit
-yto customize.)3. Install Dependencies:
We need the Express framework, the Vonage SDK, and
dotenvfor managing environment variables.express: Web framework for handling webhook requests.@vonage/server-sdk: To interact with the Vonage APIs.dotenv: To load credentials securely from a.envfile.4. Create Project Files:
Create the main files we'll be working with.
touch .env server.js send-sms.js .gitignore.env: Stores sensitive credentials (API keys, application ID, etc.).server.js: Runs the Express server to listen for webhooks.send-sms.js: Contains the logic to send an SMS message..gitignore: Specifies files/directories that Git should ignore (like.envandnode_modules).5. Configure
.gitignore:Add the following lines to your
.gitignorefile to prevent committing sensitive information and unnecessary files:6. Set Up Environment Variables (
.env):Open the
.envfile and prepare it for your Vonage credentials. We'll fill these in later..env? Storing credentials directly in code is insecure. Using environment variables loaded from.env(which is gitignored) keeps secrets out of your source control.dotenvmakes this easy in development. In production, you'd typically set these variables directly in your hosting environment.Configuring Vonage Webhooks for SMS Delivery Status
To receive delivery status callbacks and inbound messages, you must configure webhook URLs in your Vonage Application. Vonage will send HTTP POST requests to these endpoints whenever message events occur (delivery status changes, incoming messages).
1. Set Default SMS API to Messages API:
2. Run
ngrok:Before creating the Vonage Application, you need a publicly accessible URL for your local server's webhooks.
ngrokprovides this. Open a new terminal window and run:(Replace
3000if you chose a different port in.env)ngrokwill display output similar to this:Copy the
https://<random-string>.ngrok-free.appURL. This is your public base URL. Keep this terminal window running.3. Create a Vonage Application:
Node SMS Callbacks App).private.key. Save this file in your project's root directory (the same place asserver.js). This key is used by the SDK to authenticate requests for this application.ngrokForwarding URL and append/webhooks/inbound. Example:https://<random-string>.ngrok-free.app/webhooks/inboundngrokForwarding URL and append/webhooks/status. Example:https://<random-string>.ngrok-free.app/webhooks/status4. Link Your Vonage Number:
5. Update
.envFile:Now, open your
.envfile and fill in the values you obtained:Ensure
VONAGE_PRIVATE_KEY_PATHcorrectly points to where you saved theprivate.keyfile.Sending SMS Messages with Node.js and Tracking Message UUIDs
Every SMS sent through the Vonage Messages API receives a unique
message_uuididentifier. This UUID is critical for tracking delivery status updates, as Vonage includes it in webhook callbacks to match status updates with the original message.File:
send-sms.js// send-sms.js require('dotenv').config(); // Load environment variables from .env file const fs = require('fs'); // Require Node.js file system module to read the private key const { Vonage } = require('@vonage/server-sdk'); // Note: Messages capability is accessed via the main Vonage instance // --- Configuration --- const vonageApiKey = process.env.VONAGE_API_KEY; const vonageApiSecret = process.env.VONAGE_API_SECRET; const vonageApplicationId = process.env.VONAGE_APPLICATION_ID; const vonagePrivateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH; const vonageNumber = process.env.VONAGE_NUMBER; const toNumber = process.env.TO_NUMBER; // --- Input Validation (Basic) --- if (!vonageApiKey || !vonageApiSecret || !vonageApplicationId || !vonagePrivateKeyPath || !vonageNumber || !toNumber) { console.error('Error: Missing required environment variables. Check your .env file.'); process.exit(1); // Exit if configuration is incomplete } let privateKey; try { privateKey = fs.readFileSync(vonagePrivateKeyPath); } catch (err) { console.error(`Error reading private key file at path: ${vonagePrivateKeyPath}`); console.error(err.message); process.exit(1); // Exit if private key cannot be read } // --- Initialize Vonage Client --- // Note: For Messages API with Application ID and Private Key, // you initialize the main Vonage client slightly differently. // The API Key and Secret are used for authentication along with the Application ID and Private Key. const vonage = new Vonage({ apiKey: vonageApiKey, apiSecret: vonageApiSecret, // Secret is still needed for some auth mechanisms, like signed webhooks applicationId: vonageApplicationId, privateKey: privateKey }); // --- Define the send SMS function --- async function sendSms() { console.log(`Attempting to send SMS from ${vonageNumber} to ${toNumber}...`); try { // Use vonage.messages.send() for the Messages API const resp = await vonage.messages.send({ message_type: "text", text: "Hello from Vonage and Node.js!", to: toNumber, from: vonageNumber, channel: "sms" }); console.log('SMS Sent Successfully!'); console.log('Message UUID:', resp.messageUuid); // Save this UUID for tracking delivery status } catch (err) { console.error('Error sending SMS:'); if (err.response?.data) { // Log detailed Vonage API error if available console.error('Vonage API Error:', JSON.stringify(err.response.data, null, 2)); } else { // Log the general error object console.error(err); } } } // --- Execute the function --- sendSms();Explanation:
.envvariables and the Node.jsfsmodule.process.env.fs.readFileSync. Includes error handling if the file is missing or unreadable.Vonageinstance, passing the API key, secret, application ID, and the content of the private key.sendSmsFunction:asyncfunction to handle the asynchronous API call.vonage.messages.send()which is the correct method for the Messages API.message_type,text,to,from, andchannel.try...catchblock for error handling.messageUuidfrom the successful response (resp.messageUuid) - this UUID is essential for correlating delivery status updates.sendSmsfunction to initiate the process.Building Express Webhook Endpoints for SMS Delivery Status
Create Express.js endpoints to receive Vonage webhook callbacks. You'll need two separate endpoints: one for delivery status updates (
/webhooks/status) and one for inbound messages (/webhooks/inbound).File:
server.js// server.js require('dotenv').config(); const express = require('express'); const app = express(); // Vonage needs to POST JSON data to the webhooks app.use(express.json()); // Sometimes data might be URL-encoded (less common for Messages API webhooks) app.use(express.urlencoded({ extended: true })); const PORT = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Webhook Endpoints --- // Endpoint for Inbound SMS Messages app.post('/webhooks/inbound', (req, res) => { console.log('--- Inbound SMS Received ---'); console.log('From:', req.body.from?.number || req.body.from); // Handle potential structure variations console.log('To:', req.body.to?.number || req.body.to); console.log('Message:', req.body.message?.content?.text || req.body.text); // Handle potential structure variations console.log('Full Body:', JSON.stringify(req.body, null, 2)); console.log('--------------------------\n'); // Vonage requires a 200 OK response to acknowledge receipt of the webhook res.status(200).end(); }); // Endpoint for Delivery Status Updates app.post('/webhooks/status', (req, res) => { console.log('--- Delivery Status Update ---'); console.log('Message UUID:', req.body.message_uuid); console.log('Status:', req.body.status); console.log('Timestamp:', req.body.timestamp); if (req.body.error) { console.error('Error Code:', req.body.error.code); console.error('Error Reason:', req.body.error.reason); } console.log('Full Body:', JSON.stringify(req.body, null, 2)); console.log('--------------------------\n'); // Acknowledge receipt res.status(200).end(); }); // --- Server Start --- app.listen(PORT, () => { console.log(`Server listening for webhooks at http://localhost:${PORT}`); console.log('Ensure ngrok is running and configured in the Vonage Dashboard:'); console.log('Inbound URL: /webhooks/inbound'); console.log('Status URL: /webhooks/status'); });Explanation:
require('dotenv').config();loads the variables from.env.const app = express();creates the Express application.app.use(express.json());: Parses incoming requests with JSON payloads (which Vonage uses for webhooks).app.use(express.urlencoded({ extended: true }));: Parses incoming requests with URL-encoded payloads.app.post('/webhooks/inbound', ...): Defines a handler for POST requests to the/webhooks/inboundpath. It logs relevant information from the request body (req.body).app.post('/webhooks/status', ...): Defines a handler for POST requests to the/webhooks/statuspath. It logs the delivery status information including themessage_uuidto correlate with sent messages.res.status(200).end();: This is critical. Vonage expects a200 OKresponse quickly to acknowledge receipt. Failure to send this promptly will cause Vonage to retry the webhook.app.listen(PORT, ...)starts the server on the specified port.Implementing Proper Error Handling and Logging
The current code includes basic
console.logstatements. For production, you'd want more robust logging and error handling.Error Handling (
send-sms.js):The
try...catchblock in the updatedsend-sms.jsalready handles errors during the API call more robustly. You can enhance this further:// Example enhancement in send-sms.js catch block (already improved in Section 3) // ... inside the catch block ... console.error("Error sending SMS:"); // Log specific Vonage error details if available if (err.response?.data) { console.error("Vonage Error:", JSON.stringify(err.response.data, null, 2)); } else { console.error(err); // Log the generic error } // Implement retry logic here if needed (e.g., for network errors) // Send error notification (e.g., to Sentry, Datadog) // ...Logging (
server.js):Replace
console.logwith a dedicated logging library likewinstonorpinofor structured logging, different log levels (info, warn, error), and log rotation.// Example using Winston in server.js (simplified) const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console({ format: winston.format.simple() }), // Add file transport for production // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], }); // Replace console.log with logger.info, logger.warn, logger.error // e.g., inside /webhooks/inbound // logger.info('--- Inbound SMS Received ---'); // logger.info(`From: ${req.body.from?.number || req.body.from}`); // ...etc. // In case of webhook processing error: // try { /* process webhook */ } catch (error) { // logger.error('Error processing inbound webhook:', error); // res.status(500).end(); // Respond with an error, but be cautious as Vonage might retry // } app.listen(PORT, () => { logger.info(`Server listening for webhooks at http://localhost:${PORT}`); // ... });Retry Mechanisms (Vonage):
200 OK. Ensure your webhook endpoints respond quickly. Offload time-consuming processing..catchblock insend-sms.jsif necessary (e.g., for transient network errors), potentially using exponential backoff.Database Schema and Data Layer (Conceptual)
This guide focuses on the core integration. In a real application, you would store message details and status updates in a database.
Conceptual Schema (e.g., PostgreSQL):
CREATE TABLE sms_messages ( message_uuid UUID PRIMARY KEY, -- Vonage Message UUID vonage_number VARCHAR(20) NOT NULL, recipient_number VARCHAR(20) NOT NULL, message_text TEXT, direction VARCHAR(10) NOT NULL, -- 'outbound' or 'inbound' status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, rejected vonage_status_timestamp TIMESTAMPTZ, -- Timestamp from status webhook error_code VARCHAR(10), error_reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Index for querying by status or recipient CREATE INDEX idx_sms_messages_status ON sms_messages(status); CREATE INDEX idx_sms_messages_recipient ON sms_messages(recipient_number);Data Layer Implementation:
vonage.messages.send()succeeds, insert a record withdirection='outbound',status='submitted', and themessage_uuid.message_uuidand update itsstatus,vonage_status_timestamp, etc.direction='inbound',status='delivered', and details from the payload.Adding Security Features
.envand.gitignore. Use environment variables or secrets management in production. Never commit secrets.X-Vonage-Signatureheader in incoming webhook requests.VONAGE_API_SECRETand compare it to the header value.@vonage/server-sdkmay offer utilities, but manual implementation might be needed. Implementing this is crucial for production security.ngrokprovides HTTPS. Ensure your production deployment uses HTTPS for webhook URLs.Understanding SMS Delivery Status Values
Vonage delivery status webhooks include a
statusfield indicating the current message state:submitted: Message accepted by Vonage and sent to the carrier networkdelivered: Carrier confirmed the message reached the recipient's devicefailed: Delivery failed (network issues, invalid number, etc.)rejected: Message rejected by carrier (often due to content filtering)expired: Message expired before delivery (recipient phone off for extended period)Important: Not all carriers support delivery receipts. In some countries/networks, you may only receive
submittedstatus. Always handle cases wheredeliveredstatus never arrives.Additional Implementation Considerations
message_type: 'text'+14155550101) for international compatibilityImplementing Performance Optimizations
200 OKimmediately. Offload slow processing (DB writes, external calls) to background jobs (e.g.,bullmq,agenda, AWS SQS).sms_messages) on frequently queried columns (message_uuid,status, numbers).pm2for process management and clustering.Adding Monitoring, Observability, and Analytics
/healthendpoint returning200 OK.Troubleshooting Common Issues
VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATHin.env. Ensure the private key file exists and is readable.ngrokIssues:ngrokruns and points to the correct local port (3000).ngrokForwarding URL in the Vonage Application webhook settings is current (it changes on restart for free accounts).ngrokweb interface (http://127.0.0.1:4040) for requests/errors.3000).200 OK: Check server logs (server.jsoutput) for errors if Vonage retries webhooks. Ensure quick responses.submittedconfirms handoff to the carrier.private.key: Ensure theprivate.keyfile matches theVONAGE_APPLICATION_IDand its content is correct.+1...) for phone numbers.Deployment and CI/CD
ngrok: Deployserver.jsto a hosting provider (Heroku, AWS, Google Cloud, etc.)..envfile. Uploadprivate.keysecurely or store its content in an environment variable (handle newlines carefully).pm2to run your Node.js app:npm install pm2 -g # Install globally pm2 start server.js --name vonage-webhooksTesting SMS Delivery Status Webhooks
1. Start Required Services:
ngrok http 3000and copy the Forwarding URLngrokURLnode server.js2. Test Sending SMS:
node send-sms.js.SMS Sent Successfully! Message UUID: <uuid>.TO_NUMBERreceives the SMS.server.jslogs a "Delivery Status Update" (initiallysubmitted, then potentiallydelivered).3. Test Receiving Inbound SMS:
VONAGE_NUMBER.server.jslogs an "Inbound SMS Received" entry.ngrokweb interface (http://127.0.0.1:4040) for request details.4. Test Error Cases:
TO_NUMBERto an invalid format insend-sms.js, run it, and observe script errors and potentialrejected/failedstatus updates.server.js, send an SMS viasend-sms.js. Observe webhook failures inngrok. Restartserver.jsto receive queued webhooks.Verification Checklist:
npm install)..envpopulated correctly.private.keyfile present and correct path in.env.ngrok/production URL + paths.ngrokrunning, forwarding correctly.server.jsrunning without errors.node send-sms.jssends SMS successfully.server.jslogs delivery status.VONAGE_NUMBERlogs inbound message inserver.js.200 OK.Next Steps and Production Considerations
You now have a working implementation for tracking Vonage SMS delivery status with Node.js webhooks. To prepare for production:
Related Resources