This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage Messages API and receive real-time delivery status updates through webhooks.
By the end of this tutorial, you will have a functional application capable of programmatically sending SMS and reliably tracking their delivery status – essential features for applications requiring notifications, alerts, or two-factor authentication.
Project Overview and Goals
What We're Building:
A Node.js backend application consisting of two main parts:
A script (index.js) to send an SMS message using the Vonage Messages API.
An Express server (server.js) to listen for incoming webhook requests from Vonage, specifically focusing on delivery status updates for the messages we send.
Problem Solved:
Many applications need to send SMS messages, but simply firing off an API request doesn't guarantee delivery. Mobile carriers might reject messages, or phones might be turned off. This guide addresses the need for reliable SMS delivery tracking by implementing status webhooks, providing confirmation (or failure reasons) for sent messages.
Technologies Used:
Node.js: A popular JavaScript runtime for building scalable backend applications.
Express: A minimal and flexible Node.js web application framework, ideal for creating API endpoints and webhook listeners.
Vonage Messages API: A powerful API enabling communication across multiple channels (SMS, MMS, WhatsApp, etc.). We'll use it specifically for sending SMS and leveraging its webhook capabilities for status updates.
@vonage/server-sdk: The official Vonage Node.js SDK simplifies interaction with the Vonage APIs.
dotenv: A utility to load environment variables from a .env file, keeping sensitive credentials out of source code.
ngrok: A tool to expose local development servers to the public internet, necessary for Vonage webhooks to reach our local machine during development.
System Architecture:
(This is a simplified text-based representation of the flow.)
Text
+-------------------+ +-----------------+ +----------------+ +--------------+
| Your Node.js App |----->| Vonage API |----->| Mobile Carrier |----->| User's Phone |
| (index.js) | | (Messages API) | +----------------+ +--------------+
+-------------------+ +-----------------+ |
| | (Delivery Status)
| V
+-------------------+ +-----------------+ +----------------+
| Your Express App |<-----| Vonage Webhook |<-----| Mobile Carrier |
| (server.js) | | (Status URL) | +----------------+
| Running on ngrok | +-----------------+
+-------------------+
Prerequisites:
Node.js and npm (or yarn): Installed on your system. (Download: https://nodejs.org/)
Configure .gitignore:
It's crucial to prevent committing sensitive information and unnecessary files to version control (like Git). Add the following to your .gitignore file:
Set up Environment Variables (.env.example and .env):
Environment variables allow us to configure the application without hardcoding sensitive values like API keys.
.env.example (Template for others): Add the following structure to .env.example. This file can be committed to Git as it shows what variables are needed, but contains no actual secrets.
.env
# .env.example
# Vonage API Credentials (Find on your Vonage Dashboard)
# Note: While included, Messages API auth primarily uses Application ID/Private Key below.
# Key/Secret might be used by other SDK functions or for legacy compatibility.
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
# Vonage Application Credentials (Generated when creating a Vonage Application)
# These are the primary credentials used for sending via Messages API in this guide.
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Or the actual path to your downloaded key
# Vonage Number (Must be purchased from Vonage Dashboard and linked to the Application)
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER
# Recipient Number (The phone number to send the SMS to, include country code e.g., 15551234567)
TO_NUMBER=RECIPIENT_PHONE_NUMBER
# Port for the Express server
PORT=3000
.env (Your actual secrets - DO NOT COMMIT TO GIT): Copy the contents of .env.example into .env and replace the placeholder values with your actual credentials and numbers. We'll get the VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in the next section. Leave them blank for now or use temporary placeholders.
.env
# .env (Replace with your actual values!)
# Note: While included, Messages API auth primarily uses Application ID/Private Key below.
VONAGE_API_KEY=abcdef1234567890
VONAGE_API_SECRET=fedcba0987654321
# These are the primary credentials used for sending via Messages API in this guide.
VONAGE_APPLICATION_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
VONAGE_PRIVATE_KEY_PATH=./private.key
VONAGE_NUMBER=12015550100
TO_NUMBER=15551234567
PORT=3000
Project Structure:
Your project directory should now look like this (potentially with a private.key file after Section 2):
Before writing code, we need to configure our Vonage account and application correctly. The Messages API relies on a Vonage Application for authentication and webhook configuration when sending messages.
Find API Key and Secret: On the main dashboard page, you'll find your API key and API secret. Copy these into your .env file for VONAGE_API_KEY and VONAGE_API_SECRET. (As noted previously, the Messages API primarily uses Application ID/Private Key for sending in this guide, but including these is common practice for the SDK).
Ensure You Have a Vonage Number: Navigate to Numbers > Your numbers (UI paths may change; look for similar options if needed). If you don't have one, go to Buy numbers, select your country, ensure SMS capability is checked, search, and purchase a number. Copy this number (including country code, e.g., 12015550100) into your .env file for VONAGE_NUMBER.
Create a Vonage Application:
Navigate to Applications > + Create a new application (UI paths may change; look for similar options if needed).
Give your application a descriptive name (e.g., Node SMS Status Guide App).
Click Generate public and private key. This will automatically download a private.key file. Save this file directly into your vonage-sms-status-guide project directory (or note the path). Update VONAGE_PRIVATE_KEY_PATH in your .env file accordingly (e.g., ./private.key).
Under Capabilities, toggle Messages ON.
Two fields will appear: Inbound URL and Status URL.
For Status URL, enter a placeholder for now: http://example.com/webhooks/status. We'll update this later with our ngrok URL. Use POST as the HTTP Method.
For Inbound URL, enter http://example.com/webhooks/inbound. Use POST. (We aren't receiving SMS in this guide, but the field is often required).
Scroll down to Link virtual numbers and link the Vonage number you purchased earlier to this application.
Click Create application.
You'll be taken back to the Applications list. Find your new application and copy its Application ID. Paste this into your .env file for VONAGE_APPLICATION_ID.
Verify SMS API Settings (Optional but good practice):
Navigate to Settings.
Scroll down to API Settings.
Under Default SMS Setting, ensure Messages API is selected if you intend for all SMS interactions account-wide to default to it. However, our code specifically uses the Messages API via the Application ID/Private Key, so this setting is less critical for this specific guide but good to be aware of.
Important: Do not configure the Delivery receipts (DLR) webhooks field in the main account settings if you are using the Messages API Status URL within a Vonage Application. They serve similar purposes but are configured differently (Application Status URL vs. global SMS API DLR). Using the Application Status URL is the standard way for the Messages API.
You have now configured your Vonage account, created an application, linked your number, and obtained all necessary credentials for your .env file.
3. Implementing Core Functionality: Sending SMS
Let's write the code to send an SMS message using the Vonage SDK and the credentials we've set up.
Edit index.js:
Open index.js and add the following code:
JavaScript
// index.js'use strict';
// Load environment variables from .env filerequire('dotenv').config();
// Import the Vonage SDKconst { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');
// --- Configuration ---// Retrieve credentials and numbers from environment variablesconst vonageApiKey = process.env.VONAGE_API_KEY; // Included for SDK conventionconst vonageApiSecret = process.env.VONAGE_API_SECRET; // Included for SDK conventionconst vonageApplicationId = process.env.VONAGE_APPLICATION_ID; // Primary auth for Messages APIconst vonagePrivateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH; // Primary auth for Messages APIconst vonageNumber = process.env.VONAGE_NUMBER;
const recipientNumber = process.env.TO_NUMBER;
// --- Input Validation (Basic) ---// Ensure all required environment variables are setif (!vonageApplicationId || !vonagePrivateKeyPath || !vonageNumber || !recipientNumber) {
console.error(`Error: Missing required environment variables. Check your .env file.`);
console.error(` VONAGE_APPLICATION_ID: ${vonageApplicationId ? 'Found' : 'Missing'}`);
console.error(` VONAGE_PRIVATE_KEY_PATH: ${vonagePrivateKeyPath ? 'Found' : 'Missing'}`);
console.error(` VONAGE_NUMBER: ${vonageNumber ? 'Found' : 'Missing'}`);
console.error(` TO_NUMBER: ${recipientNumber ? 'Found' : 'Missing'}`);
process.exit(1); // Exit if configuration is incomplete
}
// --- Initialize Vonage Client ---// Using Application ID and Private Key for authentication with Messages APIconst vonage = newVonage({
apiKey: vonageApiKey, // Included per SDK conventionapiSecret: vonageApiSecret, // Included per SDK conventionapplicationId: vonageApplicationId,
privateKey: vonagePrivateKeyPath,
});
// --- Send SMS Function ---asyncfunctionsendSms() {
console.log(`Attempting to send SMS from ${vonageNumber} to ${recipientNumber}...`);
// Define the message contentconst textContent = `Hello from Vonage! This is a test message sent on ${newDate().toLocaleTimeString()}. (Sent: ${Date.now()})`; // Add timestamp to make uniquetry {
// Use the Messages API to send an SMSconst resp = await vonage.messages.send(
newSMS({
to: recipientNumber,
from: vonageNumber,
text: textContent,
})
);
// Log the success response (contains message_uuid)console.log('SMS Submitted Successfully!');
console.log('Message UUID:', resp.messageUuid); // Crucial for tracking status
} catch (err) {
// Log any errors that occur during sendingconsole.error('Error sending SMS:');
// Log specific Vonage API errors if availableif (err.response && err.response.data) {
console.error('API Response Error:', JSON.stringify(err.response.data, null, 2));
} else {
console.error(err); // Log the general error object
}
process.exitCode = 1; // Indicate failure
}
}
// --- Execute Sending ---sendSms();
Code Explanation:
require('dotenv').config();: Loads variables from the .env file into process.env.
require('@vonage/server-sdk'): Imports the main Vonage SDK class.
require('@vonage/messages'): Imports the specific SMS class helper for constructing message objects, improving clarity.
Configuration Loading: Retrieves necessary credentials and numbers from process.env.
Input Validation: A basic check ensures critical environment variables are present before proceeding. Uses 'Found' : 'Missing' for clearer logging.
new Vonage(...): Initializes the Vonage client. For the Messages API, authentication primarily uses the applicationId and privateKey. The apiKey and apiSecret are included following SDK conventions but are not the primary auth method for this specific Messages API call.
sendSms() Function:
An async function to handle the asynchronous nature of the API call.
Constructs the SMS message using new SMS({...}). The to and from numbers are taken from environment variables, and a simple text message is defined. Adding a timestamp makes each message unique, which helps in testing.
vonage.messages.send(...): This is the core SDK method call to send the message via the Messages API.
Success Logging: If the await completes without throwing, the API has accepted the message for delivery. We log the messageUuid – this is the unique identifier Vonage assigns to the message, which will appear in the status webhook.
Error Handling: A try...catch block handles potential errors during the API call (e.g., network issues, invalid credentials, malformed requests). It logs detailed error information if available from the Vonage API response.
You can try running this now (node index.js), but the status updates won't work until we set up the webhook server and ngrok. You should see either a "SMS Submitted Successfully!" message with a UUID or an error in your console.
4. Implementing the Webhook Server for Status Updates
Now, we'll create the Express server that Vonage will send delivery status updates to.
Edit server.js:
Open server.js and add the following code:
JavaScript
// server.js'use strict';
// Load environment variables from .env filerequire('dotenv').config();
// Import Expressconst express = require('express');
const { json, urlencoded } = express; // Import body parsing middleware// --- Configuration ---const port = process.env.PORT || 3000; // Use port from .env or default to 3000const webhookPath = '/webhooks/status'; // The specific path for our status webhook// --- Initialize Express App ---const app = express();
// --- Middleware ---// Enable parsing of JSON request bodies (Vonage sends JSON)
app.use(json());
// Enable parsing of URL-encoded request bodies (less common for Vonage webhooks, but good practice)
app.use(urlencoded({ extended: true }));
// --- Webhook Route Handler ---// Define a POST route handler for the status webhook path
app.post(webhookPath, (req, res) => {
console.log('--- Status Webhook Received ---');
console.log('Timestamp:', newDate().toISOString());
// Log the entire request body received from Vonageconsole.log('Request Body:', JSON.stringify(req.body, null, 2));
// --- Extract Key Information (Example) ---// You would typically process this data, e.g., update a databaseconst { message_uuid, status, timestamp, to, from, error, client_ref } = req.body;
console.log(`\nExtracted Info:`);
console.log(` Message UUID: ${message_uuid}`);
console.log(` Status: ${status}`); // e.g., submitted, delivered, rejected, undeliverableconsole.log(` Timestamp: ${timestamp}`);
console.log(` To: ${to}`);
console.log(` From: ${from}`);
if (error) {
console.log(` Error Code: ${error.code}`);
console.log(` Error Reason: ${error.reason}`);
}
if (client_ref) {
console.log(` Client Reference: ${client_ref}`); // If you sent one
}
console.log('------------------------------\n');
// --- Acknowledge Receipt ---// IMPORTANT: Respond with a 2xx status code (e.g., 200 OK or 204 No Content)// to let Vonage know the webhook was received successfully.// Failure to do so will cause Vonage to retry sending the webhook.
res.status(204).send(); // 204 No Content is often suitable
});
// --- Default Route (Optional) ---// A simple root route to check if the server is running
app.get('/', (req, res) => {
res.send(`Webhook server running. Listening for POST requests on ${webhookPath}.`);
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Webhook server listening on http://localhost:${port}`);
console.log(`Expecting status webhooks at POST ${webhookPath}`);
});
Code Explanation:
require('express'): Imports the Express framework.
express.json() & express.urlencoded(): Middleware essential for parsing the incoming request body. Vonage webhooks typically send data as JSON, so express.json() is crucial.
webhookPath: Defines the URL path (/webhooks/status) where the server will listen for incoming status updates. This must match the path configured in the Vonage Application's Status URL.
app.post(webhookPath, ...): Sets up a route handler specifically for POST requests to our defined path. This is where Vonage will send the data.
Logging: Inside the handler, we log the timestamp and the entire req.body (the data sent by Vonage) to the console. This is invaluable for debugging and understanding the data structure.
Data Extraction: We demonstrate how to pull out key fields like message_uuid, status, timestamp, and potential error details from the request body. In a real application, you'd use this data (especially message_uuid and status) to update your application's state (e.g., mark a message as delivered in a database).
res.status(204).send(): Critically important. This line sends an HTTP 204 No Content response back to Vonage. Any 2xx status code tells Vonage ""I received the webhook successfully."" If you don't send a 2xx response (or if your server errors out), Vonage will assume the webhook failed and will retry sending it multiple times, potentially leading to duplicate processing.
app.listen(...): Starts the Express server, making it listen for incoming requests on the specified port.
5. Running and Verifying with ngrok
Now we tie everything together using ngrok to expose our local webhook server to the internet so Vonage can reach it. This typically involves running processes in separate terminal windows: one for ngrok, one for the webhook server (server.js), and one to run the sending script (index.js).
(Note: For managing multiple processes during development, alternatives exist like running server/script in the background using & in Bash, or using terminal multiplexers like tmux or screen, though these are beyond the scope of this basic guide.)
Start ngrok:
Open a new terminal window (keep the previous one for running the server/script). Run ngrok to forward traffic to the port your Express server will listen on (defined in .env, defaulting to 3000).
Bash
# Terminal 2: ngrok
ngrok http 3000
ngrok will display output similar to this:
Text
ngrok session starts terminal_session
Version 3.x.x
Region United States (us-cal-1)
Latency 24ms
Web Interface http://127.0.0.1:4040
Forwarding https://<RANDOM_SUBDOMAIN>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https:// Forwarding URL (e.g., https://<RANDOM_SUBDOMAIN>.ngrok-free.app). This is your public URL.
Update Vonage Application Status URL:
Go back to your Vonage Dashboard > Applications.
Find the application you created earlier and click Edit.
In the Messages capability section, paste your full ngrok Forwarding URL into the Status URL field, making sure to append the correct path: YOUR_NGROK_URL/webhooks/status.
Start the Webhook Server:
Go back to your first terminal window (where your project code is). Start the Express server:
Bash
# Terminal 1: Project Directory
node server.js
You should see:
Text
Webhook server listening on http://localhost:3000
Expecting status webhooks at POST /webhooks/status
Send a Test SMS:
Open another new terminal window (or reuse Terminal 1 after stopping the server if needed, but it's easier to keep the server running). Navigate to your project directory and run the sending script:
You should see the "SMS Submitted Successfully!" message and the messageUuid logged in this terminal.
Verify Delivery Status:
Check your recipient phone; you should receive the SMS within a few seconds to a minute.
Watch the terminal window where server.js is running (Terminal 1).
You should see log output starting with --- Status Webhook Received ---.
Initially, you might see a status like submitted.
Shortly after the message arrives on the phone, you should receive another webhook call with status: delivered.
Look for the message_uuid in the logged webhook body – it should match the UUID logged by index.js.
Example server.js output for a delivered message (Timestamps updated):
Text
Webhook server listening on http://localhost:3000
Expecting status webhooks at POST /webhooks/status
--- Status Webhook Received ---
Timestamp: 2023-10-27T10:30:15.123Z
Request Body: {
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-111111111111",
"to": "15551234567",
"from": "12015550100",
"timestamp": "2023-10-27T10:30:15.000Z",
"status": "submitted",
"channel": "sms",
"usage": {
"currency": "EUR",
"price": "0.0050"
}
}
------------------------------
--- Status Webhook Received ---
Timestamp: 2023-10-27T10:30:18.456Z
Request Body: {
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-111111111111",
"to": "15551234567",
"from": "12015550100",
"timestamp": "2023-10-27T10:30:18.000Z",
"status": "delivered",
"channel": "sms",
"usage": {
"currency": "EUR",
"price": "0.0050"
}
}
Extracted Info:
Message UUID: aaaaaaaa-bbbb-cccc-dddd-111111111111
Status: delivered
Timestamp: 2023-10-27T10:30:18.000Z
To: 15551234567
From: 12015550100
------------------------------
Congratulations! You've successfully sent an SMS and received its delivery status via a webhook.
6. Error Handling and Logging
Sending Errors (index.js): The try...catch block in index.js catches errors during the initial API call (e.g., invalid credentials, insufficient funds, malformed request). Logging err.response.data provides specific Vonage error details.
Webhook Errors (server.js):
Vonage Retries: If your server.js crashes or fails to return a 2xx status, Vonage will retry sending the webhook. Design your webhook handler to be idempotent (processing the same webhook multiple times should not cause adverse effects, e.g., check if you've already processed that message_uuid and status before updating a database).
Logging: The console.log statements are basic logging. For production, use a dedicated logging library (like winston or pino) to structure logs, write to files, and set log levels (info, warn, error).
Delivery Status Errors: The status field in the webhook payload indicates delivery issues (e.g., rejected, undeliverable). The optional error object within the payload provides carrier-specific error codes and reasons, which can be logged for debugging delivery failures.
7. Security Considerations
API Credentials: Never commit your .env file or hardcode credentials in your source code. Use environment variables and ensure .env is in your .gitignore.
Webhook Security:CRITICAL FOR PRODUCTION: Publicly accessible webhooks can be called by anyone, not just Vonage. The code provided here does not verify incoming webhooks and is vulnerable if exposed publicly without additional security. For production environments, you must implement webhook signature verification. Vonage signs its webhook requests, allowing your server to cryptographically verify that the request genuinely came from Vonage and hasn't been tampered with. Failure to do this poses a significant security risk. Refer to the official Vonage documentation on Signed Webhooks for detailed instructions and implementation examples using libraries like jsonwebtoken or Node.js's built-in crypto module.
Input Validation: Sanitize and validate any data extracted from webhooks after signature verification, before using it in database queries or other critical operations to prevent injection attacks or unexpected behavior.
8. Troubleshooting and Caveats
ngrok Not Working: Ensure ngrok is running and you've correctly copied the https:// URL and appended /webhooks/status in the Vonage Application settings. Check firewalls if issues persist. Remember ngrok URLs change each time you restart it (unless you have a paid plan with static domains).
No Webhooks Received:
Verify the Status URL in the Vonage Application is correct (HTTPS, correct path, POST method).
Ensure server.js is running and listening on the correct port (ngrok forwards to).
Check the ngrok web interface (http://127.0.0.1:4040 by default) for incoming requests and potential errors.
Confirm the Vonage number is linked to the correct Vonage Application.
Incorrect Credentials Error: Double-check VONAGE_APPLICATION_ID and the path to VONAGE_PRIVATE_KEY_PATH in your .env file. Ensure the private.key file exists at that path and has the correct read permissions. Also verify VONAGE_API_KEY and VONAGE_API_SECRET if the SDK throws errors related to them.
SMS Not Sent/Received:
Verify TO_NUMBER and VONAGE_NUMBER in .env are correct (include country code, no dashes/spaces).
Check your Vonage account balance.
Ensure the destination country/carrier is supported by Vonage and doesn't have restrictions on your Vonage number type.
Check index.js logs for specific API errors.
Delivery Receipt (DLR) Delays/Missing: DLRs depend on downstream carriers reporting back to Vonage. Delays can occur. Not all carriers or number types reliably provide DLRs (especially for statuses beyond submitted). The delivered status is the most reliable confirmation.
Multiple Webhook Calls for One Message: You will typically receive multiple status updates for a single message (e.g., submitted, then later delivered or failed). Your handler logic should account for this sequence.
9. Deployment and CI/CD (Conceptual)
Hosting: Deploy server.js to a hosting provider (like Heroku, AWS EC2/Lambda, Google Cloud Run, DigitalOcean App Platform).
Environment Variables: Configure your production environment variables securely through your hosting provider's interface (do not include the .env file in your deployment package). Remember to include private.key content securely (e.g., via environment variable or secure file storage).
Webhook URL: Update the Vonage Application Status URL to point to your live production server's public URL (e.g., https://your-app-domain.com/webhooks/status). ngrok is only for local development.
Webhook Security: Implement signature verification in your production deployment as discussed in Section 7.
Process Management: Use a process manager like pm2 to keep your Node.js server running reliably in production.
CI/CD: Set up a pipeline (using GitHub Actions, GitLab CI, Jenkins, etc.) to automatically test, build, and deploy your application upon code changes.
10. Verification and Testing
Environment Setup: Ensure .env is correctly populated with valid credentials and numbers.
ngrok Running: Confirm ngrok is active and forwarding to the correct local port (e.g., 3000).
Vonage Config: Double-check Application Status URL matches ngrok + /webhooks/status and uses POST.
Frequently asked questions
Use the Vonage Messages API with the Node.js SDK. After setting up a Vonage application and linking a number, initialize the Vonage client with your credentials. Then, use vonage.messages.send() with an SMS object containing recipient, sender, and message content to programmatically send SMS messages.
The Vonage Messages API is a versatile API that allows developers to send and receive messages across various communication channels like SMS, MMS, WhatsApp, and Viber. This tutorial focuses on its SMS capabilities and webhook functionality for delivery status updates.
Simply sending an SMS doesn't guarantee it reaches the recipient. Network issues, carrier rejections, or device problems can prevent delivery. Tracking delivery status with webhooks ensures you know if a message was successful or why it failed, crucial for apps needing reliable notifications.
ngrok is essential during local development to create a temporary, public HTTPS URL that Vonage can access to send webhook updates to your local machine. For production, you'll deploy to a server and use its public domain.
Set up an Express server with a route that handles POST requests at the path you defined in your Vonage Application's Status URL (e.g. /webhooks/status). Vonage will send JSON data to this URL with the delivery status.
A Vonage Application ID is a unique identifier associated with your Vonage application, acting as the primary method for authenticating with the Messages API along with your private key. You create Vonage Applications in the Vonage Dashboard.
Create a Vonage Application in the Vonage dashboard, link your Vonage number to the application, and configure the Status URL under the Messages capability. This URL points to a route on your server that will receive delivery status updates.
Environment variables store sensitive information (like API keys) outside your code. Load them in your Node.js app using dotenv to prevent hardcoding credentials. This makes your code safer and more portable.
The message_uuid is a unique ID assigned by Vonage to each SMS message sent. It's included in the webhook payload and is essential for matching status updates to the original message sent by your application.
A 2xx HTTP status code (e.g. 204 No Content) signals to Vonage that you received the webhook successfully. If Vonage doesn't get this confirmation (e.g. if your server crashes), it will retry sending the same webhook multiple times, potentially leading to duplicate processing in your application.
Use ngrok to create a public URL for your local server. Configure the ngrok URL as the Status URL in your Vonage Application. Run your Express server and send a test SMS. Monitor the server console for incoming webhook requests.
Vonage webhooks will report statuses like 'submitted', 'delivered', 'rejected', or 'undeliverable', giving you insights into the message lifecycle. The 'error' object within the payload can offer carrier-specific codes and reasons for failure.
Yes, the Vonage Messages API is well-suited for two-factor authentication. The reliable delivery tracking ensures that users receive verification codes even if network issues arise. Use webhooks to confirm delivery status in your system.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage Messages API and receive real-time delivery status updates through webhooks.
By the end of this tutorial, you will have a functional application capable of programmatically sending SMS and reliably tracking their delivery status – essential features for applications requiring notifications, alerts, or two-factor authentication.
Project Overview and Goals
What We're Building:
A Node.js backend application consisting of two main parts:
index.js) to send an SMS message using the Vonage Messages API.server.js) to listen for incoming webhook requests from Vonage, specifically focusing on delivery status updates for the messages we send.Problem Solved:
Many applications need to send SMS messages, but simply firing off an API request doesn't guarantee delivery. Mobile carriers might reject messages, or phones might be turned off. This guide addresses the need for reliable SMS delivery tracking by implementing status webhooks, providing confirmation (or failure reasons) for sent messages.
Technologies Used:
@vonage/server-sdk: The official Vonage Node.js SDK simplifies interaction with the Vonage APIs.dotenv: A utility to load environment variables from a.envfile, keeping sensitive credentials out of source code.ngrok: A tool to expose local development servers to the public internet, necessary for Vonage webhooks to reach our local machine during development.System Architecture:
(This is a simplified text-based representation of the flow.)
Prerequisites:
ngrok: Installed and authenticated. A free account is sufficient. (Download: https://ngrok.com/download)Expected Outcome:
A local Node.js application that can:
submitted,delivered,rejected) for sent messages in the server console.1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for the project, then navigate into it.
# Terminal mkdir vonage-sms-status-guide cd vonage-sms-status-guideInitialize Node.js Project: This creates a
package.jsonfile to manage project dependencies and scripts.# Terminal npm init -yInstall Dependencies: We need the Vonage SDK, the Express framework, and
dotenvfor environment variables.# Terminal npm install @vonage/server-sdk express dotenvCreate Project Files: Create the main files for our sending script and our webhook server.
# Terminal (Linux/macOS) touch index.js server.js .env .env.example .gitignore # Terminal (Windows - PowerShell) New-Item index.js, server.js, .env, .env.example, .gitignore -ItemType FileConfigure
.gitignore: It's crucial to prevent committing sensitive information and unnecessary files to version control (like Git). Add the following to your.gitignorefile:Set up Environment Variables (
.env.exampleand.env): Environment variables allow us to configure the application without hardcoding sensitive values like API keys..env.example(Template for others): Add the following structure to.env.example. This file can be committed to Git as it shows what variables are needed, but contains no actual secrets..env(Your actual secrets - DO NOT COMMIT TO GIT): Copy the contents of.env.exampleinto.envand replace the placeholder values with your actual credentials and numbers. We'll get theVONAGE_APPLICATION_IDandVONAGE_PRIVATE_KEY_PATHin the next section. Leave them blank for now or use temporary placeholders.Project Structure:
Your project directory should now look like this (potentially with a
private.keyfile after Section 2):2. Integrating with Vonage (Configuration)
Before writing code, we need to configure our Vonage account and application correctly. The Messages API relies on a Vonage Application for authentication and webhook configuration when sending messages.
Log in to Vonage Dashboard: Access your dashboard at https://dashboard.nexmo.com/.
Find API Key and Secret: On the main dashboard page, you'll find your
API keyandAPI secret. Copy these into your.envfile forVONAGE_API_KEYandVONAGE_API_SECRET. (As noted previously, the Messages API primarily uses Application ID/Private Key for sending in this guide, but including these is common practice for the SDK).Ensure You Have a Vonage Number: Navigate to Numbers > Your numbers (UI paths may change; look for similar options if needed). If you don't have one, go to Buy numbers, select your country, ensure SMS capability is checked, search, and purchase a number. Copy this number (including country code, e.g.,
12015550100) into your.envfile forVONAGE_NUMBER.Create a Vonage Application:
Node SMS Status Guide App).private.keyfile. Save this file directly into yourvonage-sms-status-guideproject directory (or note the path). UpdateVONAGE_PRIVATE_KEY_PATHin your.envfile accordingly (e.g.,./private.key).http://example.com/webhooks/status. We'll update this later with ourngrokURL. UsePOSTas the HTTP Method.http://example.com/webhooks/inbound. UsePOST. (We aren't receiving SMS in this guide, but the field is often required)..envfile forVONAGE_APPLICATION_ID.Verify SMS API Settings (Optional but good practice):
You have now configured your Vonage account, created an application, linked your number, and obtained all necessary credentials for your
.envfile.3. Implementing Core Functionality: Sending SMS
Let's write the code to send an SMS message using the Vonage SDK and the credentials we've set up.
Edit
index.js: Openindex.jsand add the following code:// index.js 'use strict'; // Load environment variables from .env file require('dotenv').config(); // Import the Vonage SDK const { Vonage } = require('@vonage/server-sdk'); const { SMS } = require('@vonage/messages'); // --- Configuration --- // Retrieve credentials and numbers from environment variables const vonageApiKey = process.env.VONAGE_API_KEY; // Included for SDK convention const vonageApiSecret = process.env.VONAGE_API_SECRET; // Included for SDK convention const vonageApplicationId = process.env.VONAGE_APPLICATION_ID; // Primary auth for Messages API const vonagePrivateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH; // Primary auth for Messages API const vonageNumber = process.env.VONAGE_NUMBER; const recipientNumber = process.env.TO_NUMBER; // --- Input Validation (Basic) --- // Ensure all required environment variables are set if (!vonageApplicationId || !vonagePrivateKeyPath || !vonageNumber || !recipientNumber) { console.error(`Error: Missing required environment variables. Check your .env file.`); console.error(` VONAGE_APPLICATION_ID: ${vonageApplicationId ? 'Found' : 'Missing'}`); console.error(` VONAGE_PRIVATE_KEY_PATH: ${vonagePrivateKeyPath ? 'Found' : 'Missing'}`); console.error(` VONAGE_NUMBER: ${vonageNumber ? 'Found' : 'Missing'}`); console.error(` TO_NUMBER: ${recipientNumber ? 'Found' : 'Missing'}`); process.exit(1); // Exit if configuration is incomplete } // --- Initialize Vonage Client --- // Using Application ID and Private Key for authentication with Messages API const vonage = new Vonage({ apiKey: vonageApiKey, // Included per SDK convention apiSecret: vonageApiSecret, // Included per SDK convention applicationId: vonageApplicationId, privateKey: vonagePrivateKeyPath, }); // --- Send SMS Function --- async function sendSms() { console.log(`Attempting to send SMS from ${vonageNumber} to ${recipientNumber}...`); // Define the message content const textContent = `Hello from Vonage! This is a test message sent on ${new Date().toLocaleTimeString()}. (Sent: ${Date.now()})`; // Add timestamp to make unique try { // Use the Messages API to send an SMS const resp = await vonage.messages.send( new SMS({ to: recipientNumber, from: vonageNumber, text: textContent, }) ); // Log the success response (contains message_uuid) console.log('SMS Submitted Successfully!'); console.log('Message UUID:', resp.messageUuid); // Crucial for tracking status } catch (err) { // Log any errors that occur during sending console.error('Error sending SMS:'); // Log specific Vonage API errors if available if (err.response && err.response.data) { console.error('API Response Error:', JSON.stringify(err.response.data, null, 2)); } else { console.error(err); // Log the general error object } process.exitCode = 1; // Indicate failure } } // --- Execute Sending --- sendSms();Code Explanation:
require('dotenv').config();: Loads variables from the.envfile intoprocess.env.require('@vonage/server-sdk'): Imports the main Vonage SDK class.require('@vonage/messages'): Imports the specificSMSclass helper for constructing message objects, improving clarity.process.env.'Found' : 'Missing'for clearer logging.new Vonage(...): Initializes the Vonage client. For the Messages API, authentication primarily uses theapplicationIdandprivateKey. TheapiKeyandapiSecretare included following SDK conventions but are not the primary auth method for this specific Messages API call.sendSms()Function:asyncfunction to handle the asynchronous nature of the API call.new SMS({...}). Thetoandfromnumbers are taken from environment variables, and a simple text message is defined. Adding a timestamp makes each message unique, which helps in testing.vonage.messages.send(...): This is the core SDK method call to send the message via the Messages API.awaitcompletes without throwing, the API has accepted the message for delivery. We log themessageUuid– this is the unique identifier Vonage assigns to the message, which will appear in the status webhook.try...catchblock handles potential errors during the API call (e.g., network issues, invalid credentials, malformed requests). It logs detailed error information if available from the Vonage API response.You can try running this now (
node index.js), but the status updates won't work until we set up the webhook server andngrok. You should see either a "SMS Submitted Successfully!" message with a UUID or an error in your console.4. Implementing the Webhook Server for Status Updates
Now, we'll create the Express server that Vonage will send delivery status updates to.
Edit
server.js: Openserver.jsand add the following code:// server.js 'use strict'; // Load environment variables from .env file require('dotenv').config(); // Import Express const express = require('express'); const { json, urlencoded } = express; // Import body parsing middleware // --- Configuration --- const port = process.env.PORT || 3000; // Use port from .env or default to 3000 const webhookPath = '/webhooks/status'; // The specific path for our status webhook // --- Initialize Express App --- const app = express(); // --- Middleware --- // Enable parsing of JSON request bodies (Vonage sends JSON) app.use(json()); // Enable parsing of URL-encoded request bodies (less common for Vonage webhooks, but good practice) app.use(urlencoded({ extended: true })); // --- Webhook Route Handler --- // Define a POST route handler for the status webhook path app.post(webhookPath, (req, res) => { console.log('--- Status Webhook Received ---'); console.log('Timestamp:', new Date().toISOString()); // Log the entire request body received from Vonage console.log('Request Body:', JSON.stringify(req.body, null, 2)); // --- Extract Key Information (Example) --- // You would typically process this data, e.g., update a database const { message_uuid, status, timestamp, to, from, error, client_ref } = req.body; console.log(`\nExtracted Info:`); console.log(` Message UUID: ${message_uuid}`); console.log(` Status: ${status}`); // e.g., submitted, delivered, rejected, undeliverable console.log(` Timestamp: ${timestamp}`); console.log(` To: ${to}`); console.log(` From: ${from}`); if (error) { console.log(` Error Code: ${error.code}`); console.log(` Error Reason: ${error.reason}`); } if (client_ref) { console.log(` Client Reference: ${client_ref}`); // If you sent one } console.log('------------------------------\n'); // --- Acknowledge Receipt --- // IMPORTANT: Respond with a 2xx status code (e.g., 200 OK or 204 No Content) // to let Vonage know the webhook was received successfully. // Failure to do so will cause Vonage to retry sending the webhook. res.status(204).send(); // 204 No Content is often suitable }); // --- Default Route (Optional) --- // A simple root route to check if the server is running app.get('/', (req, res) => { res.send(`Webhook server running. Listening for POST requests on ${webhookPath}.`); }); // --- Start Server --- app.listen(port, () => { console.log(`Webhook server listening on http://localhost:${port}`); console.log(`Expecting status webhooks at POST ${webhookPath}`); });Code Explanation:
require('express'): Imports the Express framework.express.json()&express.urlencoded(): Middleware essential for parsing the incoming request body. Vonage webhooks typically send data as JSON, soexpress.json()is crucial.webhookPath: Defines the URL path (/webhooks/status) where the server will listen for incoming status updates. This must match the path configured in the Vonage Application's Status URL.app.post(webhookPath, ...): Sets up a route handler specifically forPOSTrequests to our defined path. This is where Vonage will send the data.req.body(the data sent by Vonage) to the console. This is invaluable for debugging and understanding the data structure.message_uuid,status,timestamp, and potentialerrordetails from the request body. In a real application, you'd use this data (especiallymessage_uuidandstatus) to update your application's state (e.g., mark a message as delivered in a database).res.status(204).send(): Critically important. This line sends an HTTP204 No Contentresponse back to Vonage. Any2xxstatus code tells Vonage ""I received the webhook successfully."" If you don't send a2xxresponse (or if your server errors out), Vonage will assume the webhook failed and will retry sending it multiple times, potentially leading to duplicate processing.app.listen(...): Starts the Express server, making it listen for incoming requests on the specified port.5. Running and Verifying with
ngrokNow we tie everything together using
ngrokto expose our local webhook server to the internet so Vonage can reach it. This typically involves running processes in separate terminal windows: one forngrok, one for the webhook server (server.js), and one to run the sending script (index.js).(Note: For managing multiple processes during development, alternatives exist like running server/script in the background using
&in Bash, or using terminal multiplexers liketmuxorscreen, though these are beyond the scope of this basic guide.)Start
ngrok: Open a new terminal window (keep the previous one for running the server/script). Runngrokto forward traffic to the port your Express server will listen on (defined in.env, defaulting to 3000).# Terminal 2: ngrok ngrok http 3000ngrokwill display output similar to this:Copy the
https://Forwarding URL (e.g.,https://<RANDOM_SUBDOMAIN>.ngrok-free.app). This is your public URL.Update Vonage Application Status URL:
ngrokForwarding URL into the Status URL field, making sure to append the correct path:YOUR_NGROK_URL/webhooks/status.https://<RANDOM_SUBDOMAIN>.ngrok-free.app/webhooks/statusPOST.Start the Webhook Server: Go back to your first terminal window (where your project code is). Start the Express server:
# Terminal 1: Project Directory node server.jsYou should see:
Send a Test SMS: Open another new terminal window (or reuse Terminal 1 after stopping the server if needed, but it's easier to keep the server running). Navigate to your project directory and run the sending script:
# Terminal 3 (or reuse Terminal 1): Project Directory node index.jsYou should see the "SMS Submitted Successfully!" message and the
messageUuidlogged in this terminal.Verify Delivery Status:
server.jsis running (Terminal 1).--- Status Webhook Received ---.submitted.status: delivered.message_uuidin the logged webhook body – it should match the UUID logged byindex.js.Example
server.jsoutput for a delivered message (Timestamps updated):Congratulations! You've successfully sent an SMS and received its delivery status via a webhook.
6. Error Handling and Logging
index.js): Thetry...catchblock inindex.jscatches errors during the initial API call (e.g., invalid credentials, insufficient funds, malformed request). Loggingerr.response.dataprovides specific Vonage error details.server.js):server.jscrashes or fails to return a2xxstatus, Vonage will retry sending the webhook. Design your webhook handler to be idempotent (processing the same webhook multiple times should not cause adverse effects, e.g., check if you've already processed thatmessage_uuidandstatusbefore updating a database).console.logstatements are basic logging. For production, use a dedicated logging library (likewinstonorpino) to structure logs, write to files, and set log levels (info, warn, error).statusfield in the webhook payload indicates delivery issues (e.g.,rejected,undeliverable). The optionalerrorobject within the payload provides carrier-specific error codes and reasons, which can be logged for debugging delivery failures.7. Security Considerations
.envfile or hardcode credentials in your source code. Use environment variables and ensure.envis in your.gitignore.jsonwebtokenor Node.js's built-incryptomodule.8. Troubleshooting and Caveats
ngrokNot Working: Ensurengrokis running and you've correctly copied thehttps://URL and appended/webhooks/statusin the Vonage Application settings. Check firewalls if issues persist. RememberngrokURLs change each time you restart it (unless you have a paid plan with static domains).server.jsis running and listening on the correct port (ngrokforwards to).ngrokweb interface (http://127.0.0.1:4040by default) for incoming requests and potential errors.VONAGE_APPLICATION_IDand the path toVONAGE_PRIVATE_KEY_PATHin your.envfile. Ensure theprivate.keyfile exists at that path and has the correct read permissions. Also verifyVONAGE_API_KEYandVONAGE_API_SECRETif the SDK throws errors related to them.TO_NUMBERandVONAGE_NUMBERin.envare correct (include country code, no dashes/spaces).index.jslogs for specific API errors.submitted). Thedeliveredstatus is the most reliable confirmation.submitted, then laterdeliveredorfailed). Your handler logic should account for this sequence.9. Deployment and CI/CD (Conceptual)
server.jsto a hosting provider (like Heroku, AWS EC2/Lambda, Google Cloud Run, DigitalOcean App Platform)..envfile in your deployment package). Remember to includeprivate.keycontent securely (e.g., via environment variable or secure file storage).https://your-app-domain.com/webhooks/status).ngrokis only for local development.pm2to keep your Node.js server running reliably in production.10. Verification and Testing
.envis correctly populated with valid credentials and numbers.ngrokRunning: Confirmngrokis active and forwarding to the correct local port (e.g., 3000).ngrok+/webhooks/statusand usesPOST.