code examples
code examples
Vonage SMS Delivery Status Callbacks: Complete Node.js + Express Guide
Step-by-step guide to sending SMS with Vonage Messages API and receiving delivery status webhooks using Node.js, Express, and ngrok. Includes JWT webhook verification, error handling, and production deployment.
Developer guide: Sending SMS and receiving delivery status callbacks with Node.js, Express, and Vonage
Build a Node.js application using Express to send SMS messages via the Vonage Messages API and receive delivery status webhooks with real-time tracking.
What are delivery status callbacks? When you send an SMS, you need to know whether it reached the recipient. Delivery status callbacks (also called Delivery Receipt Reports or DLRs) are HTTP POST requests Vonage sends to your server containing the message status – delivered, failed, rejected, or other states. Without these callbacks, you're sending messages blind with no confirmation of successful delivery.
Why do you need them? Track delivery success rates, alert customers when critical messages fail, implement retry logic for failed sends, maintain audit logs for compliance, and provide users with real-time delivery feedback.
Project Overview and Goals
Build a Node.js application that demonstrates 2 core functionalities:
- Sending an SMS Message: Programmatically send an SMS message to a specified phone number using the Vonage Messages API.
- Receiving Delivery Status Updates: Set up a webhook endpoint to receive real-time status updates (e.g.,
delivered,failed,rejected) for the messages you send.
Track delivery success for critical notifications like password resets, 2FA codes, appointment reminders, order confirmations, and payment alerts where knowing the delivery status directly impacts your application's reliability and user experience.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework for creating the webhook endpoint.
- Vonage Messages API: A unified API for sending messages across various channels, including SMS. Use it for its robustness and multi-channel capabilities.
@vonage/server-sdk: The official Vonage Node.js SDK (version 3.24.1 as of January 2025) for interacting with the API.dotenv: A module to load environment variables from a.envfile for secure credential management.ngrok: A tool to expose local development servers to the internet, necessary for testing webhooks during local development. Production deployments use your server's public URL.
System Architecture:
+-----------------+ +---------------------+ +-----------------+
| Your Node.js App|----->| Vonage Messages API |----->| Carrier Network |-----> User's Phone
| (Express Server)| | (sends SMS) | | |
| - Send Script | +---------------------+ +-------+---------+
| - Webhook Ep. |<-----+ (status update) (delivery report) |
+-----------------+ | |
^ | |
| +------------------------------------+
| (ngrok tunnel for local dev)
+-------+---------+
| ngrok Service |
+-----------------+
(Note: For published documentation, replace this ASCII diagram with an image for consistent rendering across platforms.)
Final Outcome:
By the end of this guide, you'll have a functional Node.js application capable of:
- Sending an SMS message via a simple script.
- Running an Express server with a webhook endpoint (
/webhooks/status). - Receiving and logging delivery status updates sent by Vonage to your webhook.
Expected time: 45–60 minutes for developers familiar with Node.js and Express. If you're new to webhooks or SMS APIs, allow 90 minutes.
Prerequisites:
- Node.js and npm: Node.js 18.x or higher (LTS version recommended). Download Node.js
- Vonage API Account: Sign up for a free account. Vonage Signup
- You need your API Key and API Secret.
- You need to create a Vonage Application and get its Application ID and generate/download a Private Key.
- You need at least 1 Vonage virtual phone number capable of sending SMS.
ngrok: Install and authenticate. Download ngrok (A free account is sufficient).- A personal phone number to receive test SMS messages.
Pricing: Vonage charges per SMS sent (~$0.0070–$0.15 depending on destination country). New accounts receive free trial credit ($2.00 as of January 2025). Check current pricing at Vonage SMS Pricing.
1. Node.js Project Setup and Dependency Installation
Initialize the project, install dependencies, and set up the basic structure.
-
Create Project Directory: Open your terminal 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: Initialize npm to create a
package.jsonfile.bashnpm init -y -
Install Dependencies: Install
expressfor the web server,@vonage/server-sdkto interact with the Vonage API, anddotenvto manage environment variables.bashnpm install express @vonage/server-sdk dotenvNote: This installs
@vonage/server-sdkversion 3.24.1 (latest as of January 2025). This version uses TypeScript for improved code completion and follows modern async/await patterns. -
Create Project Structure: Create the necessary files and directories.
bashtouch index.js send-sms.js .env .gitignoreindex.js: Main file for the Express server (webhook listener).send-sms.js: Script to trigger sending an SMS message..env: Stores sensitive credentials (API keys, etc.)..gitignore: Specifies files/directories Git should ignore.
-
Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing dependencies and sensitive credentials.text# .gitignore node_modules/ .env private.key # Or your actual private key filenameNote: Add
private.keyassuming you might save the downloaded key file directly in the project root during development.Warning: While convenient for local development, storing private keys directly in the project folder carries risks. Never commit your private key file to version control (Git). Ensure
.gitignorecorrectly lists the key file name. -
Configure
.envFile: Open the.envfile and add placeholders for your Vonage credentials. Fill these in later.dotenv# .env VONAGE_API_KEY=YOUR_API_KEY_HERE VONAGE_API_SECRET=YOUR_API_SECRET_HERE VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE VONAGE_PRIVATE_KEY_PATH=./private.key # Ensure this matches the actual path and filename of your downloaded private key VONAGE_NUMBER=14155552671 # Your Vonage virtual number in E.164 format (country code + number, no spaces or symbols) # Add the recipient number for testing the send script TO_NUMBER=14155559876 # Recipient phone number in E.164 format- Replace
YOUR_API_KEY_HERE,YOUR_API_SECRET_HERE,YOUR_APPLICATION_ID_HERE, and the phone numbers with your actual values. - E.164 format: Country code (1 for US/Canada) followed by area code and number with no spaces, dashes, or parentheses. Example:
14155552671for a US number.
- Replace
2. Vonage Account Configuration and API Credentials
Configure your Vonage account, create an application, and obtain the necessary credentials.
-
Log in to Vonage Dashboard: Access your Vonage API Dashboard.
-
Find API Key and Secret: Your API Key and Secret appear prominently on the main dashboard page. Copy these values and paste them into your
.envfile forVONAGE_API_KEYandVONAGE_API_SECRET. -
Set Default SMS API: Ensure your account uses the Messages API for sending SMS, as this guide relies on it.
- Navigate to Account Settings in the left sidebar.
- Scroll to the API Settings section.
- Under Default SMS Setting, select Messages API.
- Click Save changes.
- Why? Vonage has legacy APIs. Selecting Messages API ensures consistency in sending behavior and webhook formats used in this guide.
-
Create a Vonage Application: The Messages API requires an application context for authentication using a private key.
- Navigate to Applications in the left sidebar.
- Click + Create a new application.
- Give your application a name (e.g.,
SMS Status Guide App). - Click Generate public and private key. This automatically downloads the
private.keyfile. Save this file securely. For development, place it in your project's root directory (as configured in.env). Vonage typically names thisprivate.key– ensure the path you set in.envmatches the exact filename and location where you saved it. - Security Note: Placing the key in the project root is shown for development simplicity. For production, store keys securely outside your codebase (e.g., using secrets management tools) and ensure strict file permissions.
- What is the private key for? Vonage uses JWT (JSON Web Token) authentication for the Messages API. Your application signs API requests with the private key to prove its identity. The corresponding public key (stored in Vonage's system) verifies these signatures. This is more secure than sending API credentials with every request.
- Enable the Messages capability.
- You'll see fields for Status URL and Inbound URL. Fill these later when you have your
ngrokURL. For now, leave them blank or enter temporary placeholders likehttp://example.com/status. - Click Generate new application.
- You'll be taken to the application details page. Copy the Application ID and paste it into your
.envfile forVONAGE_APPLICATION_ID.
-
Link Your Vonage Number: Link your Vonage virtual number to the application you just created.
- On the application details page, scroll to the Link virtual numbers section.
- Find your Vonage number in the list and click the Link button next to it.
- Copy your Vonage virtual number (in E.164 format) and paste it into your
.envfile forVONAGE_NUMBER. - Don't have a number? Navigate to Numbers > Buy numbers in the left sidebar. Search for available numbers in your country. Purchase or rent a number ($0.90/month for US numbers as of January 2025). Ensure the number supports SMS capability.
-
Update
.envwith Private Key Path: EnsureVONAGE_PRIVATE_KEY_PATHin your.envfile correctly points to where you saved the downloaded private key file (e.g.,./private.keyif it's in the root and namedprivate.key). Match the actual filename if it differs.
Your .env file should now contain your actual credentials (except for the webhook URLs, handled later).
3. Implementing SMS Sending with Vonage Messages API
Write the script to send an SMS message using the Vonage Node.js SDK and the Messages API.
-
Edit
send-sms.js: Open thesend-sms.jsfile and add the following code:javascript// send-sms.js require('dotenv').config(); // Load environment variables from .env file const { Vonage } = require('@vonage/server-sdk'); const { Auth } = require('@vonage/auth'); // --- Configuration --- const VONAGE_API_KEY = process.env.VONAGE_API_KEY; const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET; const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID; const VONAGE_PRIVATE_KEY_PATH = process.env.VONAGE_PRIVATE_KEY_PATH; const VONAGE_NUMBER = process.env.VONAGE_NUMBER; const TO_NUMBER = process.env.TO_NUMBER; // Recipient number from .env // Input validation basic check if (!VONAGE_API_KEY || !VONAGE_API_SECRET || !VONAGE_APPLICATION_ID || !VONAGE_PRIVATE_KEY_PATH || !VONAGE_NUMBER || !TO_NUMBER) { console.error("Error: Missing required environment variables. Check your .env file."); process.exit(1); // Exit if configuration is missing } // --- Initialize Vonage --- // Use Application ID and Private Key for authentication with Messages API const credentials = new Auth({ apiKey: VONAGE_API_KEY, apiSecret: VONAGE_API_SECRET, applicationId: VONAGE_APPLICATION_ID, privateKey: VONAGE_PRIVATE_KEY_PATH, }); const options = {}; // Optional: Add any client options here if needed const vonage = new Vonage(credentials, options); // --- Send SMS Function --- async function sendSms(textMessage) { console.log(`Attempting to send SMS from ${VONAGE_NUMBER} to ${TO_NUMBER}`); try { const resp = await vonage.messages.send({ message_type: "text", text: textMessage, to: TO_NUMBER, from: VONAGE_NUMBER, channel: "sms" }); console.log(`SMS submitted successfully! Message UUID: ${resp.messageUuid}`); return resp.messageUuid; // Return the ID for potential tracking } catch (err) { console.error("Error sending SMS:"); // Log the detailed error response if available if (err.response && err.response.data) { console.error(JSON.stringify(err.response.data, null, 2)); } else { console.error(err); } throw err; // Re-throw the error for upstream handling if needed } } // --- Execute Sending --- // Immediately invoke the function when the script runs (async () => { const messageContent = `Hello from Vonage! Sent at: ${new Date().toLocaleTimeString()}`; try { await sendSms(messageContent); console.log("Script finished."); } catch (error) { console.error("Script failed to send SMS."); process.exit(1); // Exit with error code if sending failed } })(); // IIFE (Immediately Invoked Function Expression) to run async code -
Code Explanation:
require('dotenv').config(): Loads variables from the.envfile intoprocess.env.- Configuration: Retrieves necessary credentials and numbers from
process.env. Includes basic validation. - Initialize Vonage:
- Use
AuthwithapplicationIdandprivateKey– this is the required authentication method for sending via the Messages API when an application context is needed (standard practice). While the API Key/Secret are included in theAuthobject, the Application ID and Private Key primarily drive authentication for this Messages API operation; the Key/Secret might be leveraged by other SDK functionalities or legacy API interactions. new Vonage(credentials, options)creates the Vonage client instance.
- Use
sendSmsFunction:- An
asyncfunction handles the asynchronous API call. vonage.messages.send({...}): The core method call.message_type: "text": Specifies a standard text message.text: The SMS content. Maximum 160 characters for single-segment SMS using GSM-7 encoding. Longer messages split into multiple segments (up to 1,530 characters across 10 segments). Using Unicode characters reduces limit to 70 characters per segment.to: The recipient's phone number (from.env).from: Your Vonage virtual number (from.env).channel: "sms": Explicitly defines the channel.
- Success: Logs the
messageUuid– a unique identifier (format:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) that tracks this specific message through its lifecycle. Store this UUID to correlate webhook status updates with sent messages. - Error Handling: Uses a
try...catchblock to catch errors during the API call. Logs detailed error information if available in the Vonage response (err.response.data).
- An
- Execute Sending: An IIFE runs the
async sendSmsfunction immediately when you execute the script (node send-sms.js).
4. Building Express Webhook Endpoint for Delivery Status Callbacks
Create the Express server and the webhook endpoint (/webhooks/status) to receive delivery status updates from Vonage.
-
Edit
index.js: Openindex.jsand add the following code for the Express server:javascript// index.js require('dotenv').config(); // Load environment variables first const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Use environment port or default to 3000 // --- Middleware --- // Vonage sends webhooks as JSON app.use(express.json()); // Optional: Handle URL-encoded data if needed, though status webhooks are usually JSON app.use(express.urlencoded({ extended: true })); // --- Webhook Endpoint for Delivery Status --- // Vonage POSTs status updates to this route app.post('/webhooks/status', (req, res) => { const statusUpdate = req.body; console.log("--- Received Status Update ---"); // Log the entire payload for inspection console.log(JSON.stringify(statusUpdate, null, 2)); // Extract key information const { message_uuid, status, timestamp, to, from, error } = statusUpdate; console.log(`Status for message ${message_uuid}: ${status}`); if (timestamp) console.log(`Timestamp: ${timestamp}`); if (to) console.log(`To: ${to}`); if (from) console.log(`From: ${from}`); // Handle specific statuses if (status === 'delivered') { console.log("Message successfully delivered!"); // IMPLEMENTATION POINT: Update your application state (e.g., database) } else if (status === 'failed' || status === 'rejected') { console.error(`Message delivery failed or rejected.`); if (error) { console.error(`Reason: Code ${error.code} – ${error.reason}`); } // IMPLEMENTATION POINT: Trigger alerts or retry logic if applicable } else { console.log(`Received status: ${status}`); } // --- IMPORTANT: Respond to Vonage --- // Always send a 2xx status code quickly to acknowledge receipt. // Failure to do so causes Vonage to retry the webhook delivery. res.status(200).send('OK'); // Alternatively use res.status(204).send(); for "No Content" response }); // --- Basic Health Check Endpoint --- app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`Webhook endpoint available at /webhooks/status`); console.log(`Health check available at /health`); console.log("Waiting for status updates from Vonage…"); }); -
Code Explanation:
- Initialization: Sets up Express and defines the port.
- Middleware: Express processes middleware in order from top to bottom. Place
express.json()before route definitions to ensure request bodies parse correctly before handlers execute.express.json(): Parses incoming JSON request bodies (Vonage status webhooks use JSON).express.urlencoded(): Parses URL-encoded bodies (less common for status webhooks but included for completeness).
/webhooks/statusEndpoint (POST):- This is the route Vonage sends status updates to.
req.body: Contains the JSON payload from Vonage.- Logging: Logs the entire payload and key fields like
message_uuid,status,timestamp– crucial for debugging. - Status Handling: Includes example
if/else ifblocks demonstrating how you might react to different statuses (delivered,failed,rejected). Integrate with your application's logic here (e.g., updating a database). TheIMPLEMENTATION POINTcomments highlight these areas. - Response (
res.status(200).send('OK')): Critically important. You must respond with a2xxstatus code (like 200 or 204) quickly. If Vonage doesn't receive a timely2xxresponse, it assumes delivery failed and retries sending the webhook, leading to duplicate processing.
/healthEndpoint (GET): A simple endpoint to check if the server is running.- Server Start: Starts the Express server listening on the specified port.
Complete webhook payload structure:
{
"message_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"to": "14155559876",
"from": "14155552671",
"timestamp": "2025-01-15T18:30:45.123Z",
"status": "delivered",
"channel": "sms",
"message_type": "text",
"client_ref": "optional-reference-you-set",
"usage": {
"price": "0.0070",
"currency": "EUR"
},
"error": {
"code": 10,
"reason": "Illegal sender address - rejected"
}
}Timeout: Vonage waits 10 seconds for your webhook endpoint to respond with a 2xx status code. If your endpoint doesn't respond within 10 seconds, Vonage retries the webhook. Implement retry logic as described in Section 6.
5. Local Development with ngrok Tunneling for Webhook Testing
To receive webhooks from Vonage on your local machine, expose your local server to the internet using ngrok.
-
Start Your Local Server: Open a terminal in your project directory and run:
bashnode index.jsYou should see "Server listening on port 3000…" (or your configured port).
-
Start
ngrok: Open a second terminal window (leave the first one running your server). Runngrokto tunnel traffic to your local server's port (e.g., 3000).bashngrok http 3000 -
Get
ngrokURL:ngrokdisplays output similar to this:Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding https://<random-string>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00Copy the
https://<random-string>.ngrok-free.appURL. This is your public webhook URL. -
Update Vonage Application Status URL:
- Go back to your Vonage Application settings in the dashboard.
- Find the application you created earlier (
SMS Status Guide App). - Click the Edit button.
- In the Capabilities section, find the Messages capability.
- Paste your full
ngrokURL into the Status URL field, appending/webhooks/status. It should look like:https://<random-string>.ngrok-free.app/webhooks/status - (Optional but recommended): Set the Inbound URL to
https://<random-string>.ngrok-free.app/webhooks/inboundif you plan to handle incoming SMS replies later (requires adding a/webhooks/inboundroute inindex.js). - Scroll down and click Save changes.
-
Verification: Send a Test SMS:
-
Go back to the terminal where your
node index.jsserver is not running. -
Run the send script again:
bashnode send-sms.js -
Note the
messageUuidlogged by the script.
-
-
Observe Webhook:
- Watch the terminal where your
node index.jsserver is running. - Within a few seconds to minutes (depending on the carrier), you should see the "--- Received Status Update ---" log message, followed by the JSON payload from Vonage.
- Verify that the
message_uuidin the webhook payload matches the one logged bysend-sms.js. - Check the
statusfield (e.g.,submitted,delivered,failed). You might receive multiple updates for a single message as it progresses.
- Watch the terminal where your
-
Inspect with
ngrokWeb Interface: Open thengrokWeb Interface (usuallyhttp://127.0.0.1:4040) in your browser to see incoming requests to your tunneled endpoint in real-time, inspect headers, and replay requests for debugging.
Troubleshooting ngrok connection issues:
- "ngrok not found": Ensure ngrok is installed and in your system PATH. Run
ngrok versionto verify. - "Failed to listen on localhost:3000": Your Express server isn't running. Start it with
node index.jsin a separate terminal. - "Account limit reached": Free ngrok accounts allow 1 simultaneous tunnel. Close other ngrok instances.
- "Connection refused": Check firewall settings. Ensure port 3000 isn't blocked.
6. Production-Ready Error Handling and Retry Logic
While basic logging is in place, enhance it for production:
Implement error handling middleware:
// Add this after all routes in index.js
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});Robust Logging: Replace console.log with a structured logging library like Winston or Pino. Log levels (info, warn, error), timestamps, and request IDs make debugging easier. Log status updates to a persistent store or logging service.
Webhook Retries: Vonage retries webhook delivery using this policy:
- Timeout: 10 seconds for your endpoint to respond with
2xx - Retry attempts: Up to 10 retries over 72 hours
- Retry intervals: 1 minute, 5 minutes, 30 minutes, 1 hour, 4 hours, 8 hours, 12 hours, 24 hours (exponential backoff)
Ensure your endpoint responds quickly. If your processing logic is slow, acknowledge the request immediately (res.status(200).send()) and perform the processing asynchronously (e.g., using a job queue like BullMQ or Redis Queue).
Idempotency: Design your webhook handler to be idempotent. If Vonage retries a webhook (e.g., due to a temporary network issue), your handler might receive the same status update multiple times. Use the message_uuid and potentially the timestamp or a unique webhook attempt ID (if provided by Vonage) to ensure you don't process the same update twice. Store processed message_uuid+status combinations temporarily if needed.
Example idempotency check:
const processedUpdates = new Set(); // In production, use Redis or database
app.post('/webhooks/status', (req, res) => {
const { message_uuid, status, timestamp } = req.body;
const updateKey = `${message_uuid}-${status}-${timestamp}`;
if (processedUpdates.has(updateKey)) {
console.log('Duplicate webhook, ignoring');
return res.status(200).send('OK');
}
processedUpdates.add(updateKey);
// Process webhook…
});7. Webhook Security: JWT Signature Verification and Best Practices
Securing your webhook endpoint is crucial:
-
Webhook Signature Verification (Highly Recommended): Vonage uses JWT (JSON Web Token) Bearer Authorization with HMAC-SHA256 to sign webhook requests, allowing you to verify they originated from Vonage.
How Vonage Webhook Security Works:
- JWT in Authorization Header: Each webhook includes a JWT in the
Authorization: Bearer <token>header - Signature Secret: Available in your Vonage Application settings – navigate to your application in the dashboard, and you'll find the Signature Secret in the application details page. This shared secret verifies the JWT.
- Three-Layer Validation:
- JWT Signature Verification: Decode the JWT using your signature secret to confirm authenticity
- Payload Hash Check: The JWT contains a
payload_hashclaim (SHA-256 hash of the webhook body). Hash the incomingreq.bodyand compare to this claim to detect tampering - Timestamp Validation: The
iat(issued at) claim is a UTC Unix timestamp. Compare to current time to reject stale/replayed tokens
Implementation Example:
javascriptconst jwt = require('jsonwebtoken'); const crypto = require('crypto'); function verifyVonageWebhook(req, signatureSecret) { try { // Extract JWT from Authorization header const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return false; // Verify JWT signature and decode const decoded = jwt.verify(token, signatureSecret, { algorithms: ['HS256'] }); // Validate payload hash const payloadHash = crypto .createHash('sha256') .update(JSON.stringify(req.body)) .digest('hex'); if (decoded.payload_hash !== payloadHash) { console.error('Payload hash mismatch'); return false; } // Check timestamp freshness (reject if older than 5 minutes) const currentTime = Math.floor(Date.now() / 1000); if (currentTime - decoded.iat > 300) { console.error('Token too old'); return false; } return true; } catch (error) { console.error('Webhook verification failed:', error.message); return false; } } // In your webhook handler: app.post('/webhooks/status', (req, res) => { const signatureSecret = process.env.VONAGE_SIGNATURE_SECRET; // Add to .env if (!verifyVonageWebhook(req, signatureSecret)) { return res.status(401).send('Unauthorized'); } // Process webhook… const statusUpdate = req.body; // … rest of handler });Note: Install
jsonwebtokenpackage:npm install jsonwebtoken - JWT in Authorization Header: Each webhook includes a JWT in the
-
HTTPS:
ngrokprovides HTTPS URLs, and your production deployment must use HTTPS to protect data in transit. -
IP Allowlisting: As an additional layer (or alternative if signature verification isn't feasible), configure your firewall or load balancer to only allow requests to your webhook endpoint from Vonage's known IP address ranges. Find current IP ranges at Vonage IP Ranges Documentation.
-
Input Sanitization: Although the data comes from Vonage, sanitize any data from the webhook before using it in database queries or other sensitive operations to prevent potential injection attacks if the data format unexpectedly changes.
-
Rate Limiting: Implement rate limiting on your webhook endpoint (e.g., using
express-rate-limit) to prevent potential abuse or denial-of-service if your endpoint is exposed accidentally.
Rate limiting example:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.post('/webhooks/status', webhookLimiter, (req, res) => {
// Handler code…
});- Environment Variables: Never hardcode credentials. Use environment variables managed securely in your deployment environment.
8. Database Schema Design for SMS Message Tracking
For production use, store SMS details and status updates.
-
Why Store Data? Track message history, auditing, analytics, provide status feedback to users, handle retries based on failure codes.
-
Example Schema (Conceptual):
sqlCREATE TABLE sms_messages ( message_uuid VARCHAR(255) PRIMARY KEY, -- Vonage Message UUID vonage_number VARCHAR(20) NOT NULL, recipient_number VARCHAR(20) NOT NULL, message_content TEXT, submitted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, last_status VARCHAR(50), last_status_timestamp TIMESTAMPTZ, final_status VARCHAR(50), -- "delivered", "failed", "expired", etc. error_code VARCHAR(50), error_reason TEXT, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, INDEX idx_recipient (recipient_number), INDEX idx_status (last_status), INDEX idx_submitted (submitted_at) ); CREATE TABLE sms_status_updates ( id SERIAL PRIMARY KEY, message_uuid VARCHAR(255) REFERENCES sms_messages(message_uuid), status VARCHAR(50) NOT NULL, status_timestamp TIMESTAMPTZ NOT NULL, raw_payload JSONB, -- Store the full webhook payload received_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, INDEX idx_message_uuid (message_uuid), INDEX idx_received (received_at) );
Performance considerations:
- Add indexes on frequently queried columns:
recipient_number,last_status,submitted_at,message_uuid - Use
JSONBforraw_payloadto enable efficient querying of webhook data - Set appropriate
VARCHARlengths to optimize storage
Data retention: Implement a cleanup strategy to archive or delete old records:
-
Archive messages older than 90 days to separate cold storage
-
Delete status updates for archived messages after 1 year
-
Use scheduled jobs (cron or database triggers) for automated cleanup
-
Implementation:
- Use an ORM like Prisma or Sequelize to manage the schema and interactions.
- When sending (
send-sms.js): Insert a new record intosms_messageswith themessage_uuidand initial details. - In the webhook handler (
index.js):- Find the corresponding record in
sms_messagesusingmessage_uuid. - Insert a new record into
sms_status_updates. - Update the
last_status,last_status_timestamp, and potentiallyfinal_status,error_code,error_reasonin thesms_messagestable.
- Find the corresponding record in
9. Handling Special SMS Delivery Cases and Edge Scenarios
- Status Codes: Familiarize yourself with Vonage's delivery status codes (
submitted,delivered,failed,expired,rejected,accepted,unknown, etc.). Handlefailedandrejectedspecifically, potentially logging theerror.codeanderror.reasonprovided in the webhook. Consult the official Vonage documentation for detailed explanations of all status and error codes. - Message Concatenation: Long SMS messages split into multiple parts but share the same
message_uuid. Your status updates might reflect individual parts or the message as a whole depending on the carrier and Vonage processing. - Carrier Delays: Delivery receipts can be delayed or, in some rare cases (specific countries/carriers), not supported or provided. Your application should handle potential timeouts or lack of a final "delivered" status.
- Timestamp Timezones: Webhook timestamps (
timestampfield) are typically in UTC. Store them appropriately (e.g.,TIMESTAMPTZin PostgreSQL) and convert to local timezones only for display purposes. - International SMS: Different countries have varying regulations. Some require sender ID registration (India, UAE, Saudi Arabia), while others restrict certain content types. Check Vonage Country-Specific SMS Features before sending internationally.
- Unknown Status Handling: The
unknownstatus means the carrier didn't provide delivery confirmation (common in certain regions). Treat as uncertain rather than failed. Don't automatically retry, but log for monitoring. If many messages showunknown, consider alternative carriers or channels for that destination.
10. Application Monitoring and Performance Metrics
- Health Checks: The
/healthendpoint is a basic start. Production systems often require more detailed checks (e.g., database connectivity). - Metrics: Track key metrics: SMS sent count, delivery success rate (delivered / (delivered + failed + expired)), failure counts by error code, webhook processing latency. Use libraries like
prom-clientto expose metrics for Prometheus/Grafana.
Metrics implementation example:
const promClient = require('prom-client');
const register = new promClient.Registry();
const smsSentCounter = new promClient.Counter({
name: 'sms_sent_total',
help: 'Total SMS messages sent',
registers: [register]
});
const smsDeliveredCounter = new promClient.Counter({
name: 'sms_delivered_total',
help: 'Total SMS messages delivered',
registers: [register]
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});- Error Tracking: Integrate with error tracking services like Sentry or Bugsnag to capture and alert on exceptions in both the sending script and the webhook handler.
- Logging Aggregation: Use a log aggregation platform (ELK stack, Datadog Logs, Grafana Loki) to centralize logs for easier searching and analysis.
11. Vonage Delivery Status Codes and Error Reference
Understanding Vonage's Delivery Receipt (DLR) status codes is essential for proper error handling and monitoring.
Complete DLR Status Codes Reference
| Status Code | Meaning | Description | Action Required | Typical Time |
|---|---|---|---|---|
submitted | Message submitted | Vonage accepted the message and submitted it to the carrier network | Monitor for final delivery status | 1–5 seconds |
delivered (DELIVRD) | Successfully delivered | Message successfully delivered to recipient's device | No action – success case | 5–30 seconds |
failed (FAILED) | Delivery failed | Message delivery failed due to technical or carrier issues | Check error code, consider retry with exponential backoff | Varies |
rejected (REJECTD) | Message rejected | Carrier or platform rejected the message before delivery | Check error code, do not retry without fixing root cause | 1–10 seconds |
expired (EXPIRED) | Message expired | Message not delivered within validity period (carrier-dependent, typically 24–72 hours) | Consider shorter messages or alternative channels | 24–72 hours |
undelivered (UNDELIV) | Undelivered | Carrier reports message could not be delivered (device off, out of coverage) | May retry after delay or notify user | Varies |
accepted (ACCEPTD) | Accepted by carrier | Carrier accepted message but final delivery status pending | Wait for final status update | 5–15 seconds |
unknown (UNKNOWN) | Status unknown | Carrier did not provide delivery confirmation | Common for certain carriers/countries; treat as uncertain | N/A |
deleted (DELETED) | Message deleted | Message was deleted before delivery (rare) | Do not retry | N/A |
Source: Vonage DLR Statuses Documentation (accessed January 2025)
SMS Error Codes and Troubleshooting Guide
When status is failed or rejected, the webhook includes an error object with code and reason fields:
| Error Code | Description | Typical Cause |
|---|---|---|
| 1 | Throttled | Rate limit exceeded; implement exponential backoff |
| 2 | Missing parameters | Required field missing in API request |
| 3 | Invalid parameters | Parameter format incorrect (e.g., invalid phone number) |
| 4 | Invalid credentials | API key/secret incorrect or application not authorized |
| 5 | Internal error | Vonage platform issue; retry with exponential backoff |
| 6 | Invalid message | Message content violates policies or contains invalid characters |
| 7 | Number barred | Recipient number blocked/barred from receiving messages |
| 8 | Partner account barred | Your Vonage account suspended; contact support |
| 9 | Partner quota exceeded | Monthly SMS quota reached; upgrade plan or wait for reset |
| 10 | Illegal sender | Sender ID not allowed for destination country |
| 15 | Illegal sender | Sender address invalid (same as code 10) |
| 1340 | Outside allowed window | WhatsApp-specific: message sent outside 24-hour customer care window |
Important: Vonage charges for messages regardless of final delivery status, except when rejected by the Vonage platform before submission (certain error codes like 2, 3, 4).
Source: Vonage SMS Delivery Error Codes (accessed January 2025)
12. Common Issues and Debugging Solutions
ngrokIssues:- Ensure
ngrokis running and the correct URL (HTTPS) is configured in the Vonage dashboard Status URL. - Free
ngrokAccount Limitations (2025):- Bandwidth: 1 GB per month limit on free tier
- Interstitial Warning Page: Free tier injects a "Visit Site" interstitial page before all HTML traffic (sets cookie for 7 days after first visit)
- Random URLs: URLs change on restart unless using paid static domains
- Single Tunnel: Only 1 simultaneous tunnel allowed on free plan
- Free
ngrokaccounts are sufficient for development but consider paid plans for production-like testing or higher bandwidth needs. - Firewalls might block
ngrok.
- Ensure
- Vonage Configuration Errors:
- Incorrect API Credentials/App ID/Private Key: Double-check
.envvalues. Ensure the private key file path (VONAGE_PRIVATE_KEY_PATH) is correct, matches the actual filename, and the file is readable by your application. - Messages API Not Default: Verify the account setting for Default SMS Setting.
- Number Not Linked: Ensure the Vonage number is linked to the correct application.
- Incorrect Status URL: Verify the URL format (
https://…/webhooks/status) in the Vonage application settings.
- Incorrect API Credentials/App ID/Private Key: Double-check
- Code Errors:
- Check server logs (
node index.jsoutput) for errors during webhook processing. - Check send script logs (
node send-sms.jsoutput) for API errors during sending. - Ensure
dotenvis loading variables correctly (console.log(process.env.VONAGE_API_KEY)early in the script to test).
- Check server logs (
- No Webhooks Received:
- Verify the SMS was actually sent successfully (check
send-sms.jslog formessageUuid). - Check the Vonage dashboard for API logs or error reports related to webhook delivery failures.
- Confirm
ngrokis running and accessible. - Wait sufficient time – carrier delivery reports can take time.
- Verify the SMS was actually sent successfully (check
- Delivery Failures (
failed/rejectedstatus):- Check the
error.codeanderror.reasonin the webhook payload. - Consult the error codes reference table above. Common reasons include invalid recipient number format, blocked number, carrier restrictions, insufficient account funds.
- Check the
- API vs. Delivery: Remember that a successful API response from
vonage.messages.sendonly means Vonage accepted the message for delivery, not that it was delivered. The webhook provides the actual delivery status.
Manual webhook testing with curl:
curl -X POST http://localhost:3000/webhooks/status \
-H "Content-Type: application/json" \
-d '{
"message_uuid": "test-uuid-123",
"to": "14155559876",
"from": "14155552671",
"timestamp": "2025-01-15T18:30:45.123Z",
"status": "delivered",
"channel": "sms"
}'13. Production Deployment and CI/CD Pipeline Setup
- Environment Variables: In production, manage
.envvariables securely using your hosting provider's mechanism (e.g., Heroku Config Vars, AWS Secrets Manager, Docker secrets). Never commit.envfiles or private keys to Git. - Deployment:
- PaaS (Heroku, Render, etc.): Configure Procfile (
web: node index.js), push code, set environment variables via the dashboard/CLI. - Docker: Create a
Dockerfileto containerize the application. Manage theprivate.keysecurely (e.g., mount as a volume or use Docker secrets). - Server: Deploy code, install dependencies, manage environment variables, use a process manager like PM2 to keep the server running.
- PaaS (Heroku, Render, etc.): Configure Procfile (
- CI/CD Pipeline (e.g., GitHub Actions, GitLab CI):
- Lint & Format: Check code style.
- Test: Run unit and integration tests.
- Build (if needed): e.g., Build Docker image.
- Deploy: Push code/image to the hosting environment. Manage secrets carefully during deployment.
- Webhook URL in Production: Use your application's public domain name instead of the
ngrokURL in the Vonage Application Status URL settings. Ensure it's HTTPS. - Private Key Handling: The private key file needs to be securely transferred to the production environment and referenced by the
VONAGE_PRIVATE_KEY_PATHenvironment variable. Do not store it in version control.
Example Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]Example GitHub Actions workflow:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- name: Deploy to production
run: |
# Add your deployment commands here
env:
VONAGE_API_KEY: ${{ secrets.VONAGE_API_KEY }}
VONAGE_API_SECRET: ${{ secrets.VONAGE_API_SECRET }}Frequently Asked Questions About Vonage SMS Delivery Status
How do I track SMS delivery status with Vonage?
Configure a webhook endpoint in your Vonage Application settings to receive delivery status callbacks. Vonage sends POST requests with status updates (submitted, delivered, failed, rejected) to your webhook URL containing the message_uuid and status information.
What is the difference between message submitted and delivered status?
submitted means Vonage accepted your message and sent it to the carrier network. delivered confirms the carrier successfully delivered the message to the recipient's device. Always wait for delivered status to confirm actual delivery.
How long does it take to receive delivery status webhooks?
Delivery status webhooks typically arrive within seconds to minutes after sending. However, carrier delays can extend this to several minutes or hours. Some carriers in certain countries may not provide delivery receipts at all.
Why am I not receiving webhooks from Vonage?
Common causes: incorrect webhook URL in Vonage dashboard, ngrok not running, webhook endpoint not responding with 2xx status code, firewall blocking requests, or your SMS was never successfully sent (check send script logs for errors).
How do I verify webhook requests are from Vonage?
Implement JWT signature verification using the signature secret from your Vonage Application settings. Verify the JWT token in the Authorization header, validate the payload_hash claim matches your request body hash, and check the iat timestamp is recent (within 5 minutes).
What HTTP status code should my webhook return?
Always return a 2xx status code (typically 200 OK or 204 No Content) immediately to acknowledge receipt. If you don't respond with 2xx, Vonage assumes delivery failed and retries the webhook, causing duplicate processing.
Can I test webhooks locally without deploying?
Yes, use ngrok to create a public HTTPS tunnel to your local development server. Configure the ngrok URL in your Vonage Application Status URL setting to receive webhooks on your local machine during development.
What does error code 10 mean in delivery status?
Error code 10 ("Illegal sender") means the sender ID or phone number is not allowed for the destination country. Some countries require pre-registered sender IDs or only allow messages from local numbers.
How do I handle duplicate webhook deliveries?
Design your webhook handler to be idempotent. Store processed message_uuid + status combinations and check before processing. Vonage retries webhooks if it doesn't receive a 2xx response, so duplicates can occur during network issues.
Does Vonage charge for failed messages?
Yes, Vonage charges for messages regardless of final delivery status, except when rejected by the Vonage platform before submission (error codes 2, 3, 4). Carrier rejections and delivery failures still incur charges.
Frequently Asked Questions
How to send SMS messages with Node.js and Vonage
Use the Vonage Messages API with the @vonage/server-sdk in your Node.js application. The sendSms function demonstrates how to send text messages by specifying the recipient, sender, and message content. Remember to set up your API key, secret, application ID, private key path, and phone numbers correctly in your .env file.
What is the Vonage Messages API?
The Vonage Messages API is a unified interface for sending various types of messages, including SMS. It offers robust features and multi-channel capabilities, making it suitable for applications needing reliable messaging and status tracking.
Why does Vonage use webhooks for status updates?
Webhooks provide real-time delivery status updates, including 'delivered,' 'failed,' or 'rejected,' directly to your application. This approach eliminates the need for constant polling and allows for immediate responses to message statuses, enabling better reliability and logging.
When should I use ngrok for Vonage webhooks?
ngrok is essential during local development to expose your local server and receive webhooks from Vonage. For production deployments, you must use your server's public HTTPS URL configured in your Vonage application settings.
Can I track SMS delivery status with Vonage?
Yes, by setting up a webhook endpoint (e.g., /webhooks/status) in your Node.js application. Vonage will send real-time status updates to this endpoint, which you can then process to track deliveries, failures, and other events.
How to set up Vonage Messages API with Node.js
First, install the @vonage/server-sdk package. Then, configure your Vonage account, create an application, and obtain the necessary credentials (API Key, API Secret, Application ID, and Private Key). Finally, initialize the Vonage client in your Node.js code using these credentials.
What is the purpose of the private key in Vonage?
The private key, along with the application ID, is crucial for authenticating your Node.js application with the Vonage Messages API and is the standard method when an application context is needed. This ensures secure communication and prevents unauthorized access to your account.
Why is a 200 OK response important for Vonage webhooks?
Responding with a 2xx status code (like 200 OK) is mandatory to acknowledge successful receipt of the Vonage webhook. Failure to respond correctly will cause Vonage to retry the webhook, leading to potential duplicate processing.
How to handle Vonage webhook retries in Node.js
Design your webhook handler to be idempotent using the message_uuid, ensuring it can process the same status update multiple times without causing issues. If processing is lengthy, respond with 200 OK immediately and process asynchronously.
What are common Vonage SMS delivery failure reasons?
Check the error code and reason in the webhook payload. Common reasons include an invalid recipient number, the recipient blocking the number, carrier-specific restrictions, or insufficient funds in your Vonage account.
What data should I store for SMS tracking?
Store the message UUID, sender and recipient numbers, content, timestamps, status updates, and any error codes. Consider the provided conceptual database schema as a starting point.
How to verify Vonage webhook signatures
Consult the Vonage Messages API documentation. They might use HMAC-SHA256 with your API secret or a dedicated webhook signing secret. Check if the Vonage SDK offers helper functions for signature verification.
How to handle Vonage message concatenation?
Long SMS are split, but share a message UUID. Status updates may be per part or as a whole. Your logic should accommodate this, potentially grouping status updates by message UUID.
What security measures to consider when using Vonage webhooks
Implement webhook signature verification to prevent unauthorized requests. Use HTTPS, IP whitelisting if possible, and input sanitization to minimize security risks.
When does Vonage retry webhooks?
Vonage retries webhook delivery if your endpoint doesn't return a 2xx HTTP status code within a short time frame, indicating that the message hasn't been processed successfully.