How to Track SMS Delivery Status with Node.js and Vonage StatusCallback Webhooks
Complete tutorial: Track Vonage SMS delivery status in real-time using Node.js Express and webhook callbacks. Learn to handle delivery receipts, monitor message lifecycle, and implement two-way SMS messaging.
How Do You Track SMS Delivery Status with Node.js and Vonage Webhooks?
Build a Node.js application using Express to send SMS messages via the Vonage Messages API and receive real-time delivery status updates through webhooks. SMS delivery failures cost businesses an estimated 5–10% of messages due to network issues, invalid numbers, and carrier rejections. Webhooks provide the only reliable way to confirm message delivery and handle failures.
By the end of this tutorial, you'll have a functional application capable of:
Sending SMS messages programmatically using the Vonage Node.js SDK
Receiving delivery status updates (delivery receipts) for sent messages
Receiving inbound SMS messages sent to your Vonage virtual number
Build a simple Node.js application consisting of two main parts:
A script (index.js) to send an outbound SMS message using the Vonage Messages API
An Express server (server.js) to listen for incoming webhook events from Vonage, specifically:
Status Webhooks: Provide updates on the delivery status of sent messages (e.g., submitted, delivered, rejected)
Inbound Webhooks: Deliver the content of SMS messages sent to your Vonage number
Problem Solved:
Sending an SMS via an API is often a "fire and forget" operation. The initial API response only confirms that the message was accepted by the platform, not that it was delivered to the user's phone. Mobile carrier networks can introduce delays or failures. This guide addresses the need for reliable delivery confirmation by implementing status webhooks. It also provides the foundation for two-way communication by handling inbound messages.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications (v18 LTS or later recommended)
Express: A minimal and flexible Node.js web application framework used here to create webhook endpoints
Vonage Messages API: A powerful API for sending and receiving messages across various channels (SMS, MMS, WhatsApp, etc.). Focus on SMS.
Vonage Node.js SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs within a Node.js environment
ngrok: A utility to expose local development servers to the public internet, necessary for Vonage webhooks to reach your machine during development
dotenv: A module to load environment variables from a .env file into process.env
System Architecture:
graph LR
A[Developer Machine] -- 1. Run node index.js --> B(Vonage Messages API);
B -- 2. Send SMS --> C[Mobile Network];
C -- 3. Deliver SMS --> D[User Phone];
C -- 4. Send Delivery Status --> B;
B -- 5. POST Status --> E[ngrok URL];
E -- 6. Forward Status --> F[Local Express Server (server.js)];
F -- 7. Log Status --> G[Console Output];
H[User Phone] -- 8. Send Inbound SMS --> C;
C -- 9. Forward Inbound SMS --> B;
B -- 10. POST Inbound --> E;
E -- 11. Forward Inbound --> F;
F -- 12. Log Inbound Message --> G;
style A fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#9cf,stroke:#333,stroke-width:2px
style H fill:#9cf,stroke:#333,stroke-width:2px
Vonage implements a webhook retry mechanism with exponential backoff. If your server doesn't respond with a 2xx status code within 5 seconds, Vonage retries the webhook up to 5 times over approximately 24 hours.
Prerequisites:
Node.js and npm: Installed on your system (v18 LTS or later recommended, tested with v18.x and v20.x). Download Node.js
Vonage Account: A free account provides API credentials and test credits. Sign up for Vonage
Vonage Virtual Number: Rent an SMS-capable number from your Vonage dashboard.
Create the project structure and install the necessary dependencies.
Create Project Directory:
Open your terminal or command prompt and create a new directory for your project, then navigate into it.
Bash
mkdir vonage-sms-status-guide
cd vonage-sms-status-guide
Initialize Node.js Project:
This creates a package.json file to manage dependencies and project metadata.
Bash
npm init -y
Install Dependencies:
Install the Vonage SDK (@vonage/server-sdk) for API interaction, Express for webhook handling, and dotenv for environment variable management.
Bash
npm install @vonage/server-sdk express dotenv
Create Project Files:
Create the main files for sending SMS and running the webhook server.
Bash
touch index.js server.js .env .gitignore
Configure .gitignore:
Prevent sensitive information and unnecessary files from being committed to version control. Add the following lines to your .gitignore file:
Text
node_modules/
.env
*.log
private.key
Project Structure:
Your project directory should now look like this:
On the main dashboard page, you'll find your API key and API secret under the "API settings" section or a similar area.
Purpose: These credentials authenticate your account for basic API requests.
Action: Copy these values. Add them to the .env file shortly.
Create a Vonage Application:
Applications act as containers for your communication settings, including webhook URLs and security credentials (like private keys) needed for certain APIs like Messages. The Application credentials (private key + Application ID) provide enhanced security and link your messages to specific webhook endpoints, while API Key/Secret provide basic account-level authentication.
Navigate to "Your applications" in the dashboard sidebar.
Click "Create a new application".
Give your application a descriptive name (e.g., Node SMS Status Guide App).
Click "Generate public and private key". Immediately save the private.key file that downloads. For simplicity in this guide, save it directly into your project's root directory (vonage-sms-status-guide/), which matches the default path we'll use in .env. The key file's location just needs to match the VONAGE_PRIVATE_KEY_PATH variable in your .env file. We've already added private.key to .gitignore for security.
Purpose: The private key is used by the Vonage SDK to authenticate requests specifically for this application, providing a higher level of security than just API Key/Secret for the Messages API.
Enable the Messages capability.
You will see fields for Inbound URL and Status URL. We will fill these in the next step using ngrok. For now, you can leave them blank or enter placeholder URLs like http://example.com/inbound and http://example.com/status.
Click "Generate new application".
Action: Note the Application ID displayed on the application details page. Copy this value.
Link Your Vonage Number:
Associate your purchased Vonage virtual number with the application you just created so Vonage knows where to route incoming messages and which webhooks to use for status updates related to that number.
Go back to the details page for the application you just created.
Scroll down to the "Link virtual numbers" section (or similar).
Find your purchased Vonage number in the list and click the "Link" button next to it.
Purpose: This tells Vonage to use the webhook URLs defined in this application when messages are sent from this number (for status updates) or to this number (for inbound messages).
Configure .env File:
Open the .env file and add your credentials. Replace the placeholder values with your actual keys, ID, number, and your test phone number. Ensure your Vonage number and TO_NUMBER are in E.164 format (without the leading '+').
Text
# Vonage API Credentials
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application Credentials
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
# Relative path from project root to your private key file
VONAGE_PRIVATE_KEY_PATH=./private.key
# Phone Numbers (Use E.164 format without leading '+', e.g., 14155552671)
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
TO_NUMBER=YOUR_PERSONAL_CELL_PHONE_NUMBER
Validate your environment variables before running the application. The Node.js script includes validation logic that checks for missing values and exits with an error message if any required variables are undefined or empty.
Security Note: The .env file should never be committed to public repositories. Ensure .env is listed in your .gitignore file.
How Do You Expose Your Local Server with ngrok?
Vonage needs to send webhook events (status updates, inbound messages) to a publicly accessible URL. During development, ngrok creates a secure tunnel from the internet to your local machine. For production, replace ngrok with a permanent public URL from your hosting provider (Heroku, AWS, DigitalOcean, etc.).
Start ngrok:
Open a new terminal window/tab (keep your project terminal open). Run ngrok to forward traffic to the port your Express server will listen on (use port 3000).
Bash
ngrok http 3000
Copy the Forwarding URL:ngrok will display session information, including a Forwarding URL ending in .ngrok.io or similar (use the https version). It will look something like https://<random-subdomain>.ngrok.io.
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-subdomain>.ngrok.io -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Action: Copy this https://...ngrok.io URL. Keep this ngrok terminal window running.
Update Vonage Application Webhook URLs:
Go back to your Vonage application settings in the dashboard.
Edit the application you created earlier.
In the Messages capability section:
Set Status URL to: YOUR_NGROK_FORWARDING_URL/webhooks/status (e.g., https://<random-subdomain>.ngrok.io/webhooks/status)
Set Inbound URL to: YOUR_NGROK_FORWARDING_URL/webhooks/inbound (e.g., https://<random-subdomain>.ngrok.io/webhooks/inbound)
Scroll down and click "Save changes".
Purpose: This tells Vonage where to send HTTP POST requests for status updates and inbound messages related to the numbers linked to this application.
How Do You Send SMS Messages with Vonage?
Write the code to send an SMS message using the Vonage Node.js SDK.
Edit index.js:
Add the following code to your index.js file.
JavaScript
// index.js'use strict';
// Load environment variables from .env filerequire('dotenv').config();
// Import Node.js file system module (needed for reading the private key)const fs = require('fs');
// Import the Vonage SDKconst { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');
// Validate essential environment variablesif (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_NUMBER || !process.env.TO_NUMBER) {
console.error(`Error: Required environment variables are missing. Check your .env file.`);
process.exit(1); // Exit if configuration is incomplete
}
// Initialize Vonage client// Uses Application ID and the *content* of the Private Key for authentication with Messages API v1const vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH), // Read key content from file path
}, { debug: true }); // Enable debug mode for more detailed SDK logs// Define the SMS sending functionasyncfunctionsendSms(to, from, text) {
console.log(`Attempting to send SMS from ${from} to ${to}: "${text}"`);
try {
const resp = await vonage.messages.send(
newSMS({
to: to,
from: from,
text: text,
})
);
console.log('Message sent successfully!');
console.log('Message UUID:', resp.messageUuid); // Log the unique ID for trackingreturn resp.messageUuid;
} catch (err) {
console.error('Error sending SMS:', err.response ? err.response.data : err.message);
// Log detailed error if available (e.g., API response error)if (err.response && err.response.data) {
console.error('API Error Details:', JSON.stringify(err.response.data, null, 2));
}
throw err; // Re-throw the error for potential higher-level handling
}
}
// --- Main Execution ---// Immediately Invoked Async Function Expression (async IIFE) to use await
(async () => {
try {
const messageText = `Hello from Vonage! Testing SMS delivery status. [${newDate().toLocaleTimeString()}]`;
awaitsendSms(
process.env.TO_NUMBER,
process.env.VONAGE_NUMBER,
messageText
);
console.log(`SMS send request initiated.`);
} catch (error) {
console.error(`Failed to initiate SMS sending:`, error.message);
// The detailed error is already logged within sendSms
}
})();
Code Explanation:
require('dotenv').config();: Loads variables from your .env file into process.env.
const fs = require('fs');: Imports Node.js's built-in file system module, needed to read the private key file.
require('@vonage/server-sdk'): Imports the main Vonage SDK class.
require('@vonage/messages'): Imports specific message types (like SMS) for the Messages API.
Environment Variable Check: Ensures all necessary configuration is present before proceeding.
new Vonage(...): Initializes the Vonage client. For the Messages API v1 (used for sending SMS, WhatsApp, etc.), authentication primarily relies on the applicationId and the associated privateKey content for enhanced security and linking capabilities like webhooks. The privateKey option requires the actual key data, not the file path, hence the use of fs.readFileSync(). Debug mode ({ debug: true }) provides verbose logging from the SDK, useful during development.
sendSms function:
Takes to, from, and text as arguments.
Uses vonage.messages.send() which is the core method for the Messages API.
Creates a new SMS(...) object specifying the message details. The SDK handles structuring the request correctly for the API.
Uses async/await for cleaner handling of the promise returned by send().
Logs the messageUuid upon successful submission to Vonage. This ID is crucial for correlating status updates.
Includes robust error handling, logging both generic errors and specific API response details if available.
Main Execution Block: An Immediately Invoked Async Function Expression (async IIFE) calls sendSms with values from .env.
How Do You Build the Webhook Server?
This Express server will listen for HTTP POST requests from Vonage at the URLs you configured (/webhooks/status and /webhooks/inbound).
Edit server.js:
Add the following code to set up the Express server and webhook endpoints.
JavaScript
// server.js'use strict';
require('dotenv').config(); // Load .env variablesconst express = require('express');
const app = express();
constPORT = process.env.PORT || 3000; // Use port from .env or default to 3000// --- Middleware ---// Express's built-in body parsers. Crucial for reading webhook payloads.
app.use(express.json()); // Parses incoming JSON payloads
app.use(express.urlencoded({ extended: true })); // Parses incoming URL-encoded payloads// Simple logging middleware to see incoming requests
app.use((req, res, next) => {
console.log(`\n--- Incoming Request ---`);
console.log(`[${newDate().toISOString()}] ${req.method}${req.originalUrl}`);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
// Avoid logging the full body here if it could be very large or sensitive in production// console.log('Body:', JSON.stringify(req.body, null, 2));next(); // Pass control to the next middleware/route handler
});
// --- Webhook Endpoints ---// Status Webhook Endpoint// Vonage sends POST requests here with delivery status updates
app.post('/webhooks/status', (req, res) => {
console.log('--- Status Webhook Received ---');
const params = req.body; // Body already parsed by middlewareconsole.log('Status Payload:', JSON.stringify(params, null, 2));
// Process the status update (e.g., update database, notify user)// Example: Log key informationconsole.log(`Message UUID: ${params.message_uuid}`);
console.log(`Status: ${params.status}`);
console.log(`Timestamp: ${params.timestamp}`);
if (params.error) {
console.error(`Error Code: ${params.error.code}, Reason: ${params.error.reason}`);
}
// Vonage expects a 2xx response to acknowledge receipt.// 204 No Content is appropriate as we don't need to send a body back.
res.status(204).send();
});
// Inbound Webhook Endpoint// Vonage sends POST requests here when your number receives an SMS
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound Webhook Received ---');
const params = req.body; // Body already parsed by middlewareconsole.log('Inbound Payload:', JSON.stringify(params, null, 2));
// Process the inbound message (e.g., parse command, store message, reply)// Example: Log key informationif (params.message_type === 'text') {
console.log(`From: ${params.from.number}`);
console.log(`Message Text: ${params.text}`);
console.log(`Message UUID: ${params.message_uuid}`);
console.log(`Timestamp: ${params.timestamp}`);
} else {
console.log(`Received non-text message type: ${params.message_type}`);
}
// Acknowledge receipt to Vonage
res.status(204).send();
});
// Basic Root Route for Health Check / Verification
app.get('/', (req, res) => {
console.log('Root endpoint hit (health check).');
res.status(200).send('Webhook server is running.');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening for webhooks on http://localhost:${PORT}`);
console.log(`Ensure ngrok is running and forwarding to this port.`);
console.log(`Vonage Application Status URL should be: YOUR_NGROK_URL/webhooks/status`);
console.log(`Vonage Application Inbound URL should be: YOUR_NGROK_URL/webhooks/inbound`);
});
// Basic Error Handling Middleware (should be last `app.use`)
app.use((err, req, res, next) => {
console.error(`--- Unhandled Error ---`);
console.error(err.stack);
res.status(500).send('Something broke!');
});
Code Explanation:
require('express'): Imports the Express framework.
app = express(): Creates an Express application instance.
PORT: Defines the port to listen on, defaulting to 3000.
Middleware:
express.json() and express.urlencoded(): These are essential for parsing the incoming request bodies sent by Vonage. Vonage typically sends webhook data as JSON.
Logging Middleware: A simple custom middleware to log basic details of every incoming request, helpful for debugging.
/webhooks/status Route:
Defines a POST handler for the status URL.
req.body: Contains the parsed payload from Vonage.
Logs the received payload. In a real application, you would parse this data (e.g., find the message_uuid, status, timestamp, error fields) and update your system accordingly (e.g., mark a message as delivered in a database).
res.status(204).send(): Crucial: Sends an HTTP 204 No Content response back to Vonage. Vonage requires a 2xx status code to confirm successful receipt. If it doesn't receive one within a timeout period, it will retry sending the webhook, potentially leading to duplicate processing.
/webhooks/inbound Route:
Defines a POST handler for the inbound URL.
Logs the received payload, which contains details like the sender's number (params.from.number), the message text (params.text), etc.
Also sends a 204 No Content response.
/ Route: A simple GET route for checking if the server is running via a browser or health check tool.
app.listen(): Starts the server and makes it listen for connections on the specified port.
Error Handling Middleware: A basic catch-all for errors that might occur in route handlers.
How Do You Verify and Test the Implementation?
Run the application and verify that SMS sending and webhook handling work correctly.
Start the Webhook Server:
In your primary terminal window (in the vonage-sms-status-guide directory), start the Express server.
Bash
node server.js
You should see output confirming the server is listening on port 3000.
Ensure ngrok is Running:
Check the other terminal window where you started ngrok. It should still be running and show Session Status online. If not, restart it (ngrok http 3000) and re-update the webhook URLs in your Vonage application settings if the forwarding URL changed.
Send a Test SMS:
In a third terminal window/tab (or stop and restart the server.js process after sending if you only have two), run the index.js script to send the SMS.
Bash
node index.js
Expected Output (index.js terminal):
Attempting to send SMS...
SDK debug logs (if enabled).
Message sent successfully!
Message UUID: <a-long-unique-identifier>
SMS send request initiated.
(Potentially error messages if configuration is wrong).
Check Your Phone:
You should receive the SMS message on the phone number specified in TO_NUMBER within a few seconds to a minute.
Monitor the Webhook Server Console:
Watch the terminal where server.js is running. As the message progresses through the network, Vonage will send POST requests to your /webhooks/status endpoint via ngrok.
Expected Output (server.js terminal):
You'll see the logging middleware output (--- Incoming Request ---) for each webhook call.
You will then see --- Status Webhook Received --- followed by the JSON payload.
Look for payloads with different status values. Common statuses include:
submitted: The message has been accepted by Vonage and sent towards the carrier.
delivered: The carrier confirmed successful delivery to the handset. (This is the goal!)
rejected: Vonage or the carrier rejected the message (check error field for reason).
undeliverable: The carrier could not deliver the message (e.g., invalid number, phone off for extended period).
Each status payload will contain the same message_uuid you saw logged by index.js.
(Optional) Test Inbound SMS:
Using your personal phone, send an SMS message to your Vonage virtual number (VONAGE_NUMBER).
Watch the server.js console again.
Expected Output (server.js terminal):
--- Incoming Request ---
--- Inbound Webhook Received --- followed by the JSON payload containing the message details (sender number, text content, etc.).
Verification Checklist:
server.js starts without errors.
ngrok is running and forwarding to the correct port (3000).
Vonage Application webhook URLs are correctly set to the ngrok URL + /webhooks/status and /webhooks/inbound.
Running node index.js logs a Message UUID and no errors.
SMS message is received on the target phone (TO_NUMBER).
server.js logs show incoming requests to /webhooks/status.
Status webhook payloads show the correct message_uuid and progress through statuses (e.g., submitted -> delivered).
(Optional) Sending an SMS to the Vonage number triggers logs for /webhooks/inbound in server.js.
How Do You Handle Errors, Logging, and Retries?
Error Handling:
The index.js script includes basic try...catch blocks to handle errors during SDK initialization or the send call. It logs detailed API errors if available. The server.js includes a basic final error handling middleware, but production applications should have more specific error handling within routes.
Common Vonage error codes include:
Error Code
Meaning
Solution
401
Authentication failed
Verify API Key, Secret, Application ID, and private key path
402
Low balance
Add credits to your Vonage account
422
Invalid parameters
Check phone number format (use E.164), message content, and required fields
429
Rate limit exceeded
Implement backoff and retry logic
1300
Invalid from number
Ensure sender number is owned by your account and SMS-enabled
The error object in status webhooks provides codes and reasons for delivery failures. Common delivery error codes include carrier rejections (1320), invalid destination numbers (1330), and content filtering violations (1340).
Logging:
We use console.log and console.error for basic logging. For production, consider using a dedicated logging library like Winston or Pino for structured logging, different log levels (debug, info, warn, error), and routing logs to files or external services. The Vonage SDK's debug: true option provides verbose internal logging. Disable this in production.
Webhook Retries:
Vonage automatically retries sending webhooks if your server doesn't respond with a 2xx status code within 5 seconds. The retry schedule uses exponential backoff: immediately, 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours (approximately 5 retries over 24 hours). This is why quickly sending res.status(204).send() is vital.
Your webhook handler should be idempotent – designed so that receiving the same webhook multiple times doesn't cause incorrect side effects (e.g., don't charge a user twice if you receive duplicate 'delivered' statuses). Check if you've already processed a specific message_uuid and status before taking action.
What Are the Security Considerations?
API Credentials: Never hardcode credentials in your source code. Use environment variables (.env locally, system environment variables in production) and ensure .env and private.key are in .gitignore.
Webhook Security:(IMPORTANT) The current implementation does not verify incoming webhooks. This means anyone could potentially send fake status updates or inbound messages to your endpoints. This setup is insecure and NOT suitable for production environments. Before deploying, you must secure your webhooks using JWT signature verification:
JavaScript
const { Auth } = require('@vonage/auth');
app.post('/webhooks/status', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
// Verify the JWT signature using your signature secretconst verifiedJWT = Auth.verifySignature(token, process.env.VONAGE_SIGNATURE_SECRET);
// Process the webhook only if verification succeedsconst params = req.body;
console.log('Verified Status Payload:', JSON.stringify(params, null, 2));
res.status(204).send();
} catch (err) {
console.error('Webhook verification failed:', err);
res.status(401).send('Unauthorized');
}
});
Find your signature secret in your Vonage Application settings under "Capabilities > Messages > Signed Webhooks". Follow the official Vonage Webhook Security documentation.
Input Validation: Sanitize and validate any data received from webhooks or user input before using it (e.g., check number formats, expected data types).
Rate Limiting: Protect your webhook endpoints from abuse by implementing rate limiting using middleware like express-rate-limit.
How Do You Troubleshoot Common Issues?
Ngrok Issues:
Ensure ngrok is running and hasn't timed out (free accounts have time limits).
Verify the ngrok URL in your Vonage application settings matches the current ngrok forwarding URL.
Check firewalls; sometimes corporate networks block ngrok.
Vonage Configuration Errors:
Double-check API Key, Secret, Application ID in .env.
Verify the VONAGE_PRIVATE_KEY_PATH in .env points correctly to your private.key file and that the file exists and is readable.
Ensure the Vonage number is correctly linked to the application.
Confirm the Messages capability is enabled for the application.
Webhook Not Receiving Data:
Check ngrok status and Vonage URLs (see above).
Verify the server.js is running and listening on the correct port (3000).
Check the ngrok web interface (http://127.0.0.1:4040 by default) to see if requests are hitting ngrok but maybe not reaching your server.
Ensure express.json() middleware is correctly configured in server.js.
Not all mobile networks or countries provide reliable delivery receipts back to Vonage. The delivered status is best-effort based on carrier reporting. submitted is generally very reliable. Countries with strong DLR support (>90%) include US, UK, Germany, France, and Australia. Countries with limited DLR support (<50%) include India, Brazil, and many African nations.
Number Formatting:
Always use the E.164 format for phone numbers. While the strict format includes a leading + (e.g., +14155552671), Vonage APIs typically accept the format without the + as well (e.g., 14155552671). This guide uses the format without the + in the .env file and code examples for consistency.
Character Encoding:
Standard SMS has limitations. For emojis or non-GSM characters, the message might be sent as Unicode, which reduces the character limit per SMS part. The Messages API generally handles this well.
How Do You Deploy to Production?
Deployment:
Choose a hosting provider (Heroku, AWS, Google Cloud, DigitalOcean, etc.).
Configure environment variables directly in the hosting provider's settings dashboard instead of using a .env file. Ensure VONAGE_PRIVATE_KEY_PATH points to the correct location of your key file on the server, or consider storing the key content directly in an environment variable (ensure proper handling of newlines if doing this).
Ensure your server listens on the port specified by the host (often via process.env.PORT).
Update your Vonage Application webhook URLs to use your permanent public server address instead of the ngrok URL.
Implement Webhook Security (Critical): Secure your endpoints using JWT signature verification before deploying to production.
Use a process manager like PM2 to keep your Node.js application running reliably in the background.
Set up monitoring using tools like Datadog, New Relic, or CloudWatch to track webhook delivery rates, error rates, response times, and system health.
Next Steps:
Database Integration: Store message details (message_uuid, to, from, text, timestamp) and update their status based on webhook events. Use tables to track message state transitions (sent → submitted → delivered).
Build a UI: Create a web interface to send messages and view their status history.
Implement Replies: Use the inbound webhook handler to parse incoming messages and potentially trigger automated replies using vonage.messages.send().
Add More Channels: Explore sending messages via WhatsApp or other channels supported by the Vonage Messages API.
Robust Error Handling: Implement more specific error handling and potentially alerting for critical failures. Create a dead letter queue for messages that fail after all retries.
Frequently Asked Questions
What delivery statuses can I expect from Vonage SMS?
Vonage SMS delivery statuses include submitted (message accepted by Vonage), delivered (confirmed delivery to recipient's device), rejected (message rejected by carrier), failed (delivery failed), and undeliverable (recipient number invalid or unreachable). The delivered status depends on carrier support for delivery receipts (DLRs), which varies by country and network. The submitted status is reliable and confirms Vonage accepted your message.
How long does it take to receive delivery status webhooks?
Status webhooks arrive in real-time as the message progresses through the delivery pipeline. You typically receive the submitted status within 1–2 seconds after sending. The delivered status arrives when the carrier confirms delivery, usually within seconds to minutes depending on network conditions. If you don't receive a delivered status within 15–30 minutes, the carrier likely doesn't support delivery receipts for that destination.
Why am I not receiving webhook callbacks on my local server?
Verify your ngrok tunnel is running and the forwarding URL hasn't changed (ngrok generates new URLs each time you restart). Check that your Vonage Application webhook URLs match your current ngrok URL exactly, including the /webhooks/status and /webhooks/inbound paths. Ensure your Express server is running on port 3000. Use the ngrok web interface at http://127.0.0.1:4040 to inspect incoming requests and debug connection issues.
What's the difference between the submitted and delivered status?
The submitted status means Vonage successfully accepted your message and passed it to the carrier network. The delivered status confirms the message reached the recipient's device. Think of submitted as "message sent to carrier" and delivered as "message received by user." Not all carriers provide delivery confirmation, so you may only receive submitted for some destinations.
How do I secure my webhook endpoints in production?
Never deploy webhook endpoints without security. Implement Vonage's webhook signature verification using JWT tokens. Each webhook request includes a JWT in the Authorization header signed with your signature secret. Use the @vonage/auth package to verify the signature before processing webhook data:
Additionally, implement HTTPS, rate limiting, and IP whitelisting for defense-in-depth security. Store your signature secret securely in environment variables.
Can I receive webhooks for messages sent through the Vonage Dashboard?
Yes, webhooks fire for all messages associated with your Vonage Application, regardless of how they were sent (API, Dashboard, or other tools). As long as the sending number is linked to your Application and webhook URLs are configured, you'll receive status updates and inbound messages. This allows you to track all messaging activity in one place.
What phone number format does Vonage require?
Vonage expects phone numbers in E.164 format: country code followed by subscriber number without spaces or special characters. While the formal E.164 specification includes a leading + (e.g., +14155552671), Vonage APIs accept numbers without it (e.g., 14155552671). Always include the country code – US numbers start with 1, UK numbers with 44, etc. Use libraries like libphonenumber-js to validate and format numbers correctly.
How do I handle delivery failures and retry logic?
Monitor the status webhook for rejected, failed, or undeliverable statuses. Implement exponential backoff retry logic for transient failures, waiting progressively longer between retry attempts:
For permanent failures like invalid numbers, don't retry – log the failure and alert your system. Consider implementing a fallback channel (email, push notification) for critical messages that fail SMS delivery.
What's the maximum SMS message length I can send?
A standard SMS supports 160 characters using GSM-7 encoding. Messages with special characters, emojis, or Unicode use UCS-2 encoding, reducing the limit to 70 characters per segment. Longer messages are automatically split into multiple segments. The Vonage Messages API handles segmentation automatically, but each segment counts as a separate message for billing. Keep messages under 160 characters for GSM-7 or 70 characters for Unicode to avoid segmentation.
How do I test webhooks without exposing my local server?
Use ngrok to create a secure tunnel from the internet to your local development server. Run ngrok http 3000 to expose port 3000 publicly. Ngrok provides an HTTPS URL that you configure in your Vonage Application. This allows Vonage to send webhooks to your local machine during development. The ngrok web interface at http://127.0.0.1:4040 lets you inspect all webhook requests in real-time, making debugging easier.
Can I track message delivery across multiple phone numbers?
Yes, link all your Vonage numbers to a single Vonage Application, and configure one set of webhook URLs for that Application. All messages from any linked number will trigger webhooks to your endpoints. Include the number information in your database when sending messages, then use the message_uuid from status webhooks to track which message and number the status update relates to. This centralized approach scales efficiently.
Use the Vonage Messages API and Node.js SDK. Install the @vonage/server-sdk package, initialize a Vonage client with your API credentials, and use the vonage.messages.send() method with a new SMS object containing the recipient, sender, and message text. This allows you to send text messages programmatically.
A Vonage webhook is a mechanism for receiving real-time notifications from the Vonage APIs about events related to your application, such as delivery status updates for SMS messages or incoming messages sent to your Vonage number. Your application provides URLs where Vonage sends these updates as HTTP POST requests.
Webhooks provide reliable delivery confirmation. Sending an SMS through an API is often "fire and forget," meaning the initial response only confirms the message was accepted, not delivered. Webhooks give you real-time feedback as the message travels through the mobile network to the recipient's device.
Create a Vonage application in your dashboard, enable the Messages capability, and provide the URL of your webhook endpoint for the 'Status URL' field. This URL will receive POST requests from Vonage containing delivery updates. ngrok can be used during development to expose a local server URL.
The Vonage Messages API is a versatile API enabling you to send and receive messages across various channels, including SMS, MMS, WhatsApp, and more. This tutorial demonstrates its use specifically for sending and tracking the delivery status of SMS messages.
Set up an 'Inbound URL' for your Vonage application in the dashboard. When someone sends an SMS to your Vonage number, Vonage sends an HTTP POST request to this URL containing the message content. Your application can then process this inbound message data.
You will need Node.js and npm installed, a Vonage account (free tier is available), a rented Vonage virtual number, and ngrok installed and authenticated. The Vonage account gives you API credentials and test credits. ngrok is used to expose your local development server.
Ngrok creates a public URL that tunnels to your local development server. This is necessary because Vonage's webhooks need to reach your local machine, which isn't directly accessible from the internet. Ngrok provides the bridge between your local server and Vonage's webhooks.
Do not use ngrok or unprotected webhook endpoints in production. Secure your webhooks using either Signed Webhooks with a shared secret or JWT verification as described in the Vonage documentation. This prevents unauthorized parties from sending fake requests to your server.
The message_uuid is a unique identifier for each message sent through the Vonage Messages API. It is included in both the initial API response when sending the message and in all subsequent status webhook updates. Use it to track the lifecycle of individual messages.
Vonage provides various status updates, including 'submitted' (accepted by Vonage), 'delivered' (confirmed by the carrier), 'rejected' (rejected by Vonage or the carrier), and 'undeliverable'. Consult the Vonage documentation for a complete list and explanation of these statuses.
The Vonage Node.js SDK (@vonage/server-sdk) simplifies interaction with Vonage APIs within a Node.js environment. It handles the complexities of API requests and responses, making it easier to send SMS messages, receive webhooks, and work with other Vonage services.
Yes, emojis are generally supported. However, using emojis or non-GSM characters might cause the message to be encoded as Unicode, which can reduce the maximum character count per SMS part (segment). Vonage handles this, but it's something to be aware of.
The express.json() middleware is crucial for parsing incoming JSON data. Vonage's webhook POST requests contain the payload (delivery status, inbound message data) as JSON in the request body. Without express.json(), you won't be able to access this JSON data in your route handlers.
How Do You Track SMS Delivery Status with Node.js and Vonage Webhooks?
Build a Node.js application using Express to send SMS messages via the Vonage Messages API and receive real-time delivery status updates through webhooks. SMS delivery failures cost businesses an estimated 5–10% of messages due to network issues, invalid numbers, and carrier rejections. Webhooks provide the only reliable way to confirm message delivery and handle failures.
By the end of this tutorial, you'll have a functional application capable of:
This pattern powers critical use cases like two-factor authentication (2FA), appointment reminders, order notifications, and alert systems where delivery confirmation is essential.
Project Overview and Goals
What You'll Build:
Build a simple Node.js application consisting of two main parts:
index.js) to send an outbound SMS message using the Vonage Messages APIserver.js) to listen for incoming webhook events from Vonage, specifically:submitted,delivered,rejected)Problem Solved:
Sending an SMS via an API is often a "fire and forget" operation. The initial API response only confirms that the message was accepted by the platform, not that it was delivered to the user's phone. Mobile carrier networks can introduce delays or failures. This guide addresses the need for reliable delivery confirmation by implementing status webhooks. It also provides the foundation for two-way communication by handling inbound messages.
Technologies Used:
@vonage/server-sdk): Simplifies interaction with Vonage APIs within a Node.js environment.envfile intoprocess.envSystem Architecture:
Vonage implements a webhook retry mechanism with exponential backoff. If your server doesn't respond with a 2xx status code within 5 seconds, Vonage retries the webhook up to 5 times over approximately 24 hours.
Prerequisites:
How Do You Set Up the Vonage SMS Project?
Create the project structure and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it.
mkdir vonage-sms-status-guide cd vonage-sms-status-guideInitialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.Install Dependencies: Install the Vonage SDK (
@vonage/server-sdk) for API interaction, Express for webhook handling, and dotenv for environment variable management.Create Project Files: Create the main files for sending SMS and running the webhook server.
touch index.js server.js .env .gitignoreConfigure
.gitignore: Prevent sensitive information and unnecessary files from being committed to version control. Add the following lines to your.gitignorefile:Project Structure: Your project directory should now look like this:
How Do You Integrate with Vonage?
Before writing code, configure Vonage to enable communication and provide necessary credentials.
Obtain API Key and Secret:
.envfile shortly.Create a Vonage Application: Applications act as containers for your communication settings, including webhook URLs and security credentials (like private keys) needed for certain APIs like Messages. The Application credentials (private key + Application ID) provide enhanced security and link your messages to specific webhook endpoints, while API Key/Secret provide basic account-level authentication.
Node SMS Status Guide App).private.keyfile that downloads. For simplicity in this guide, save it directly into your project's root directory (vonage-sms-status-guide/), which matches the default path we'll use in.env. The key file's location just needs to match theVONAGE_PRIVATE_KEY_PATHvariable in your.envfile. We've already addedprivate.keyto.gitignorefor security.ngrok. For now, you can leave them blank or enter placeholder URLs likehttp://example.com/inboundandhttp://example.com/status.Link Your Vonage Number: Associate your purchased Vonage virtual number with the application you just created so Vonage knows where to route incoming messages and which webhooks to use for status updates related to that number.
Configure
.envFile: Open the.envfile and add your credentials. Replace the placeholder values with your actual keys, ID, number, and your test phone number. Ensure your Vonage number andTO_NUMBERare in E.164 format (without the leading '+').Validate your environment variables before running the application. The Node.js script includes validation logic that checks for missing values and exits with an error message if any required variables are undefined or empty.
Security Note: The
.envfile should never be committed to public repositories. Ensure.envis listed in your.gitignorefile.How Do You Expose Your Local Server with ngrok?
Vonage needs to send webhook events (status updates, inbound messages) to a publicly accessible URL. During development,
ngrokcreates a secure tunnel from the internet to your local machine. For production, replace ngrok with a permanent public URL from your hosting provider (Heroku, AWS, DigitalOcean, etc.).Start ngrok: Open a new terminal window/tab (keep your project terminal open). Run
ngrokto forward traffic to the port your Express server will listen on (use port 3000).Copy the Forwarding URL:
ngrokwill display session information, including aForwardingURL ending in.ngrok.ioor similar (use thehttpsversion). It will look something likehttps://<random-subdomain>.ngrok.io.Action: Copy this
https://...ngrok.ioURL. Keep thisngrokterminal window running.Update Vonage Application Webhook URLs:
YOUR_NGROK_FORWARDING_URL/webhooks/status(e.g.,https://<random-subdomain>.ngrok.io/webhooks/status)YOUR_NGROK_FORWARDING_URL/webhooks/inbound(e.g.,https://<random-subdomain>.ngrok.io/webhooks/inbound)Purpose: This tells Vonage where to send HTTP POST requests for status updates and inbound messages related to the numbers linked to this application.
How Do You Send SMS Messages with Vonage?
Write the code to send an SMS message using the Vonage Node.js SDK.
Edit
index.js: Add the following code to yourindex.jsfile.// index.js 'use strict'; // Load environment variables from .env file require('dotenv').config(); // Import Node.js file system module (needed for reading the private key) const fs = require('fs'); // Import the Vonage SDK const { Vonage } = require('@vonage/server-sdk'); const { SMS } = require('@vonage/messages'); // Validate essential environment variables if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_NUMBER || !process.env.TO_NUMBER) { console.error(`Error: Required environment variables are missing. Check your .env file.`); process.exit(1); // Exit if configuration is incomplete } // Initialize Vonage client // Uses Application ID and the *content* of the Private Key for authentication with Messages API v1 const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH), // Read key content from file path }, { debug: true }); // Enable debug mode for more detailed SDK logs // Define the SMS sending function async function sendSms(to, from, text) { console.log(`Attempting to send SMS from ${from} to ${to}: "${text}"`); try { const resp = await vonage.messages.send( new SMS({ to: to, from: from, text: text, }) ); console.log('Message sent successfully!'); console.log('Message UUID:', resp.messageUuid); // Log the unique ID for tracking return resp.messageUuid; } catch (err) { console.error('Error sending SMS:', err.response ? err.response.data : err.message); // Log detailed error if available (e.g., API response error) if (err.response && err.response.data) { console.error('API Error Details:', JSON.stringify(err.response.data, null, 2)); } throw err; // Re-throw the error for potential higher-level handling } } // --- Main Execution --- // Immediately Invoked Async Function Expression (async IIFE) to use await (async () => { try { const messageText = `Hello from Vonage! Testing SMS delivery status. [${new Date().toLocaleTimeString()}]`; await sendSms( process.env.TO_NUMBER, process.env.VONAGE_NUMBER, messageText ); console.log(`SMS send request initiated.`); } catch (error) { console.error(`Failed to initiate SMS sending:`, error.message); // The detailed error is already logged within sendSms } })();Code Explanation:
require('dotenv').config();: Loads variables from your.envfile intoprocess.env.const fs = require('fs');: Imports Node.js's built-in file system module, needed to read the private key file.require('@vonage/server-sdk'): Imports the main Vonage SDK class.require('@vonage/messages'): Imports specific message types (likeSMS) for the Messages API.new Vonage(...): Initializes the Vonage client. For the Messages API v1 (used for sending SMS, WhatsApp, etc.), authentication primarily relies on theapplicationIdand the associatedprivateKeycontent for enhanced security and linking capabilities like webhooks. TheprivateKeyoption requires the actual key data, not the file path, hence the use offs.readFileSync(). Debug mode ({ debug: true }) provides verbose logging from the SDK, useful during development.sendSmsfunction:to,from, andtextas arguments.vonage.messages.send()which is the core method for the Messages API.new SMS(...)object specifying the message details. The SDK handles structuring the request correctly for the API.async/awaitfor cleaner handling of the promise returned bysend().messageUuidupon successful submission to Vonage. This ID is crucial for correlating status updates.sendSmswith values from.env.How Do You Build the Webhook Server?
This Express server will listen for HTTP POST requests from Vonage at the URLs you configured (
/webhooks/statusand/webhooks/inbound).Edit
server.js: Add the following code to set up the Express server and webhook endpoints.// server.js 'use strict'; require('dotenv').config(); // Load .env variables const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Middleware --- // Express's built-in body parsers. Crucial for reading webhook payloads. app.use(express.json()); // Parses incoming JSON payloads app.use(express.urlencoded({ extended: true })); // Parses incoming URL-encoded payloads // Simple logging middleware to see incoming requests app.use((req, res, next) => { console.log(`\n--- Incoming Request ---`); console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`); console.log('Headers:', JSON.stringify(req.headers, null, 2)); // Avoid logging the full body here if it could be very large or sensitive in production // console.log('Body:', JSON.stringify(req.body, null, 2)); next(); // Pass control to the next middleware/route handler }); // --- Webhook Endpoints --- // Status Webhook Endpoint // Vonage sends POST requests here with delivery status updates app.post('/webhooks/status', (req, res) => { console.log('--- Status Webhook Received ---'); const params = req.body; // Body already parsed by middleware console.log('Status Payload:', JSON.stringify(params, null, 2)); // Process the status update (e.g., update database, notify user) // Example: Log key information console.log(`Message UUID: ${params.message_uuid}`); console.log(`Status: ${params.status}`); console.log(`Timestamp: ${params.timestamp}`); if (params.error) { console.error(`Error Code: ${params.error.code}, Reason: ${params.error.reason}`); } // Vonage expects a 2xx response to acknowledge receipt. // 204 No Content is appropriate as we don't need to send a body back. res.status(204).send(); }); // Inbound Webhook Endpoint // Vonage sends POST requests here when your number receives an SMS app.post('/webhooks/inbound', (req, res) => { console.log('--- Inbound Webhook Received ---'); const params = req.body; // Body already parsed by middleware console.log('Inbound Payload:', JSON.stringify(params, null, 2)); // Process the inbound message (e.g., parse command, store message, reply) // Example: Log key information if (params.message_type === 'text') { console.log(`From: ${params.from.number}`); console.log(`Message Text: ${params.text}`); console.log(`Message UUID: ${params.message_uuid}`); console.log(`Timestamp: ${params.timestamp}`); } else { console.log(`Received non-text message type: ${params.message_type}`); } // Acknowledge receipt to Vonage res.status(204).send(); }); // Basic Root Route for Health Check / Verification app.get('/', (req, res) => { console.log('Root endpoint hit (health check).'); res.status(200).send('Webhook server is running.'); }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server listening for webhooks on http://localhost:${PORT}`); console.log(`Ensure ngrok is running and forwarding to this port.`); console.log(`Vonage Application Status URL should be: YOUR_NGROK_URL/webhooks/status`); console.log(`Vonage Application Inbound URL should be: YOUR_NGROK_URL/webhooks/inbound`); }); // Basic Error Handling Middleware (should be last `app.use`) app.use((err, req, res, next) => { console.error(`--- Unhandled Error ---`); console.error(err.stack); res.status(500).send('Something broke!'); });Code Explanation:
require('express'): Imports the Express framework.app = express(): Creates an Express application instance.PORT: Defines the port to listen on, defaulting to 3000.express.json()andexpress.urlencoded(): These are essential for parsing the incoming request bodies sent by Vonage. Vonage typically sends webhook data as JSON./webhooks/statusRoute:POSThandler for the status URL.req.body: Contains the parsed payload from Vonage.message_uuid,status,timestamp,errorfields) and update your system accordingly (e.g., mark a message as delivered in a database).res.status(204).send(): Crucial: Sends an HTTP204 No Contentresponse back to Vonage. Vonage requires a2xxstatus code to confirm successful receipt. If it doesn't receive one within a timeout period, it will retry sending the webhook, potentially leading to duplicate processing./webhooks/inboundRoute:POSThandler for the inbound URL.params.from.number), the message text (params.text), etc.204 No Contentresponse./Route: A simpleGETroute for checking if the server is running via a browser or health check tool.app.listen(): Starts the server and makes it listen for connections on the specified port.How Do You Verify and Test the Implementation?
Run the application and verify that SMS sending and webhook handling work correctly.
Start the Webhook Server: In your primary terminal window (in the
vonage-sms-status-guidedirectory), start the Express server.You should see output confirming the server is listening on port 3000.
Ensure ngrok is Running: Check the other terminal window where you started
ngrok. It should still be running and showSession Status online. If not, restart it (ngrok http 3000) and re-update the webhook URLs in your Vonage application settings if the forwarding URL changed.Send a Test SMS: In a third terminal window/tab (or stop and restart the
server.jsprocess after sending if you only have two), run theindex.jsscript to send the SMS.index.jsterminal):Attempting to send SMS...Message sent successfully!Message UUID: <a-long-unique-identifier>SMS send request initiated.Check Your Phone: You should receive the SMS message on the phone number specified in
TO_NUMBERwithin a few seconds to a minute.Monitor the Webhook Server Console: Watch the terminal where
server.jsis running. As the message progresses through the network, Vonage will send POST requests to your/webhooks/statusendpoint via ngrok.server.jsterminal):--- Incoming Request ---) for each webhook call.--- Status Webhook Received ---followed by the JSON payload.statusvalues. Common statuses include:submitted: The message has been accepted by Vonage and sent towards the carrier.delivered: The carrier confirmed successful delivery to the handset. (This is the goal!)rejected: Vonage or the carrier rejected the message (checkerrorfield for reason).undeliverable: The carrier could not deliver the message (e.g., invalid number, phone off for extended period).message_uuidyou saw logged byindex.js.(Optional) Test Inbound SMS:
VONAGE_NUMBER).server.jsconsole again.server.jsterminal):--- Incoming Request ------ Inbound Webhook Received ---followed by the JSON payload containing the message details (sender number, text content, etc.).Verification Checklist:
server.jsstarts without errors.ngrokis running and forwarding to the correct port (3000)./webhooks/statusand/webhooks/inbound.node index.jslogs aMessage UUIDand no errors.TO_NUMBER).server.jslogs show incoming requests to/webhooks/status.message_uuidand progress through statuses (e.g.,submitted->delivered)./webhooks/inboundinserver.js.How Do You Handle Errors, Logging, and Retries?
Error Handling:
The
index.jsscript includes basictry...catchblocks to handle errors during SDK initialization or thesendcall. It logs detailed API errors if available. Theserver.jsincludes a basic final error handling middleware, but production applications should have more specific error handling within routes.Common Vonage error codes include:
fromnumberThe
errorobject in status webhooks provides codes and reasons for delivery failures. Common delivery error codes include carrier rejections (1320), invalid destination numbers (1330), and content filtering violations (1340).Logging:
We use
console.logandconsole.errorfor basic logging. For production, consider using a dedicated logging library like Winston or Pino for structured logging, different log levels (debug, info, warn, error), and routing logs to files or external services. The Vonage SDK'sdebug: trueoption provides verbose internal logging. Disable this in production.Webhook Retries:
Vonage automatically retries sending webhooks if your server doesn't respond with a
2xxstatus code within 5 seconds. The retry schedule uses exponential backoff: immediately, 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours (approximately 5 retries over 24 hours). This is why quickly sendingres.status(204).send()is vital.Your webhook handler should be idempotent – designed so that receiving the same webhook multiple times doesn't cause incorrect side effects (e.g., don't charge a user twice if you receive duplicate 'delivered' statuses). Check if you've already processed a specific
message_uuidand status before taking action.What Are the Security Considerations?
.envlocally, system environment variables in production) and ensure.envandprivate.keyare in.gitignore.const { Auth } = require('@vonage/auth'); app.post('/webhooks/status', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; try { // Verify the JWT signature using your signature secret const verifiedJWT = Auth.verifySignature(token, process.env.VONAGE_SIGNATURE_SECRET); // Process the webhook only if verification succeeds const params = req.body; console.log('Verified Status Payload:', JSON.stringify(params, null, 2)); res.status(204).send(); } catch (err) { console.error('Webhook verification failed:', err); res.status(401).send('Unauthorized'); } });Find your signature secret in your Vonage Application settings under "Capabilities > Messages > Signed Webhooks". Follow the official Vonage Webhook Security documentation.
express-rate-limit.How Do You Troubleshoot Common Issues?
Ngrok Issues:
Vonage Configuration Errors:
.env.VONAGE_PRIVATE_KEY_PATHin.envpoints correctly to yourprivate.keyfile and that the file exists and is readable.Webhook Not Receiving Data:
server.jsis running and listening on the correct port (3000).http://127.0.0.1:4040by default) to see if requests are hitting ngrok but maybe not reaching your server.express.json()middleware is correctly configured inserver.js.Delivery Status Meanings:
Familiarize yourself with the possible status values and their meanings in the Vonage documentation.
Delivery Receipt (DLR) Support:
Not all mobile networks or countries provide reliable delivery receipts back to Vonage. The
deliveredstatus is best-effort based on carrier reporting.submittedis generally very reliable. Countries with strong DLR support (>90%) include US, UK, Germany, France, and Australia. Countries with limited DLR support (<50%) include India, Brazil, and many African nations.Number Formatting:
Always use the E.164 format for phone numbers. While the strict format includes a leading
+(e.g.,+14155552671), Vonage APIs typically accept the format without the+as well (e.g.,14155552671). This guide uses the format without the+in the.envfile and code examples for consistency.Character Encoding:
Standard SMS has limitations. For emojis or non-GSM characters, the message might be sent as Unicode, which reduces the character limit per SMS part. The Messages API generally handles this well.
How Do You Deploy to Production?
Deployment:
.envfile. EnsureVONAGE_PRIVATE_KEY_PATHpoints to the correct location of your key file on the server, or consider storing the key content directly in an environment variable (ensure proper handling of newlines if doing this).process.env.PORT).Next Steps:
message_uuid,to,from,text,timestamp) and update their status based on webhook events. Use tables to track message state transitions (sent → submitted → delivered).vonage.messages.send().Frequently Asked Questions
What delivery statuses can I expect from Vonage SMS?
Vonage SMS delivery statuses include
submitted(message accepted by Vonage),delivered(confirmed delivery to recipient's device),rejected(message rejected by carrier),failed(delivery failed), andundeliverable(recipient number invalid or unreachable). Thedeliveredstatus depends on carrier support for delivery receipts (DLRs), which varies by country and network. Thesubmittedstatus is reliable and confirms Vonage accepted your message.How long does it take to receive delivery status webhooks?
Status webhooks arrive in real-time as the message progresses through the delivery pipeline. You typically receive the
submittedstatus within 1–2 seconds after sending. Thedeliveredstatus arrives when the carrier confirms delivery, usually within seconds to minutes depending on network conditions. If you don't receive adeliveredstatus within 15–30 minutes, the carrier likely doesn't support delivery receipts for that destination.Why am I not receiving webhook callbacks on my local server?
Verify your ngrok tunnel is running and the forwarding URL hasn't changed (ngrok generates new URLs each time you restart). Check that your Vonage Application webhook URLs match your current ngrok URL exactly, including the
/webhooks/statusand/webhooks/inboundpaths. Ensure your Express server is running on port 3000. Use the ngrok web interface athttp://127.0.0.1:4040to inspect incoming requests and debug connection issues.What's the difference between the submitted and delivered status?
The
submittedstatus means Vonage successfully accepted your message and passed it to the carrier network. Thedeliveredstatus confirms the message reached the recipient's device. Think ofsubmittedas "message sent to carrier" anddeliveredas "message received by user." Not all carriers provide delivery confirmation, so you may only receivesubmittedfor some destinations.How do I secure my webhook endpoints in production?
Never deploy webhook endpoints without security. Implement Vonage's webhook signature verification using JWT tokens. Each webhook request includes a JWT in the
Authorizationheader signed with your signature secret. Use the@vonage/authpackage to verify the signature before processing webhook data:const { Auth } = require('@vonage/auth'); const token = req.headers.authorization?.split(' ')[1]; const verifiedJWT = Auth.verifySignature(token, process.env.VONAGE_SIGNATURE_SECRET);Additionally, implement HTTPS, rate limiting, and IP whitelisting for defense-in-depth security. Store your signature secret securely in environment variables.
Can I receive webhooks for messages sent through the Vonage Dashboard?
Yes, webhooks fire for all messages associated with your Vonage Application, regardless of how they were sent (API, Dashboard, or other tools). As long as the sending number is linked to your Application and webhook URLs are configured, you'll receive status updates and inbound messages. This allows you to track all messaging activity in one place.
What phone number format does Vonage require?
Vonage expects phone numbers in E.164 format: country code followed by subscriber number without spaces or special characters. While the formal E.164 specification includes a leading
+(e.g.,+14155552671), Vonage APIs accept numbers without it (e.g.,14155552671). Always include the country code – US numbers start with1, UK numbers with44, etc. Use libraries likelibphonenumber-jsto validate and format numbers correctly.How do I handle delivery failures and retry logic?
Monitor the status webhook for
rejected,failed, orundeliverablestatuses. Implement exponential backoff retry logic for transient failures, waiting progressively longer between retry attempts:async function sendWithRetry(to, from, text, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await sendSms(to, from, text); } catch (err) { if (attempt === maxRetries) throw err; const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s await new Promise(resolve => setTimeout(resolve, delay)); } } }For permanent failures like invalid numbers, don't retry – log the failure and alert your system. Consider implementing a fallback channel (email, push notification) for critical messages that fail SMS delivery.
What's the maximum SMS message length I can send?
A standard SMS supports 160 characters using GSM-7 encoding. Messages with special characters, emojis, or Unicode use UCS-2 encoding, reducing the limit to 70 characters per segment. Longer messages are automatically split into multiple segments. The Vonage Messages API handles segmentation automatically, but each segment counts as a separate message for billing. Keep messages under 160 characters for GSM-7 or 70 characters for Unicode to avoid segmentation.
How do I test webhooks without exposing my local server?
Use ngrok to create a secure tunnel from the internet to your local development server. Run
ngrok http 3000to expose port 3000 publicly. Ngrok provides an HTTPS URL that you configure in your Vonage Application. This allows Vonage to send webhooks to your local machine during development. The ngrok web interface athttp://127.0.0.1:4040lets you inspect all webhook requests in real-time, making debugging easier.Can I track message delivery across multiple phone numbers?
Yes, link all your Vonage numbers to a single Vonage Application, and configure one set of webhook URLs for that Application. All messages from any linked number will trigger webhooks to your endpoints. Include the number information in your database when sending messages, then use the
message_uuidfrom status webhooks to track which message and number the status update relates to. This centralized approach scales efficiently.Related Resources: