messaging channels
messaging channels
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
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:
- 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
- Status Webhooks: Provide updates on the delivery status of sent messages (e.g.,
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
.envfile intoprocess.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:2pxVonage 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.
- ngrok: Installed and authenticated. Download ngrok
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.
bashmkdir vonage-sms-status-guide cd vonage-sms-status-guide -
Initialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.bashnpm init -y -
Install Dependencies: Install the Vonage SDK (
@vonage/server-sdk) for API interaction, Express for webhook handling, and dotenv for environment variable management.bashnpm install @vonage/server-sdk express dotenv -
Create Project Files: Create the main files for sending SMS and running the webhook server.
bashtouch 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.gitignorefile:textnode_modules/ .env *.log private.key -
Project Structure: Your project directory should now look like this:
textvonage-sms-status-guide/ ├── node_modules/ ├── .env ├── .gitignore ├── index.js ├── package-lock.json ├── package.json └── server.js
How Do You Integrate with Vonage?
Before writing code, configure Vonage to enable communication and provide necessary credentials.
-
Obtain API Key and Secret:
- Log in to your Vonage API Dashboard.
- 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
.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.
- 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.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.- 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 likehttp://example.com/inboundandhttp://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
.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 '+').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_NUMBERValidate 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, 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
ngrokto forward traffic to the port your Express server will listen on (use port 3000).bashngrok http 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.textSession 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.00Action: Copy this
https://...ngrok.ioURL. Keep thisngrokterminal 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)
- Set Status URL to:
- 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 yourindex.jsfile.javascript// 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.- 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 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:- Takes
to,from, andtextas 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/awaitfor cleaner handling of the promise returned bysend(). - Logs the
messageUuidupon 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.
- Takes
- Main Execution Block: An Immediately Invoked Async Function Expression (async IIFE) calls
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/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 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.- Middleware:
express.json()andexpress.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/statusRoute:- Defines a
POSThandler 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,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.
- Defines a
/webhooks/inboundRoute:- Defines a
POSThandler 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 Contentresponse.
- Defines a
/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.- 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-guidedirectory), start the Express server.bashnode server.jsYou 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.bashnode index.js- Expected Output (
index.jsterminal):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).
- Expected Output (
-
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.- Expected Output (
server.jsterminal):- 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
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).
- Each status payload will contain the same
message_uuidyou saw logged byindex.js.
- You'll see the logging middleware output (
- Expected Output (
-
(Optional) Test Inbound SMS:
- Using your personal phone, send an SMS message to your Vonage virtual number (
VONAGE_NUMBER). - Watch the
server.jsconsole again. - Expected Output (
server.jsterminal):--- Incoming Request ------ Inbound Webhook Received ---followed by the JSON payload containing the message details (sender number, text content, etc.).
- Using your personal phone, send an SMS message to your Vonage virtual number (
Verification Checklist:
-
server.jsstarts without errors. -
ngrokis running and forwarding to the correct port (3000). - Vonage Application webhook URLs are correctly set to the ngrok URL +
/webhooks/statusand/webhooks/inbound. - Running
node index.jslogs aMessage UUIDand no errors. - SMS message is received on the target phone (
TO_NUMBER). -
server.jslogs show incoming requests to/webhooks/status. - Status webhook payloads show the correct
message_uuidand progress through statuses (e.g.,submitted->delivered). - (Optional) Sending an SMS to the Vonage number triggers logs for
/webhooks/inboundinserver.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 (
.envlocally, system environment variables in production) and ensure.envandprivate.keyare 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:
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.
- 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_PATHin.envpoints correctly to yourprivate.keyfile 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.jsis running and listening on the correct port (3000). - Check the ngrok web interface (
http://127.0.0.1:4040by default) to see if requests are hitting ngrok but maybe not reaching your server. - Ensure
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 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
.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). - 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:
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 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:
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 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.
Related Resources:
- Vonage Messages API Documentation
- SMS Delivery Status Reference
- Webhook Security Guide
- ngrok Documentation
- How to Send SMS with Node.js
- Understanding SMS Delivery Receipts