code examples
code examples
Node.js SMS Delivery Status Callbacks with Vonage
Learn how to build a Node.js application using Express and the Vonage Messages API to send SMS and receive real-time delivery status updates via webhooks.
Tracking the delivery status of SMS messages is crucial for applications that rely on timely communication. Knowing whether a message reached the recipient's handset, was rejected by the carrier, or failed for other reasons enables developers to build more robust and reliable systems. This guide provides a step-by-step walkthrough for building a Node.js application using Express and the Vonage Messages API to send SMS messages and receive real-time delivery status updates via webhooks.
We will build a simple Node.js server that can send an SMS message and expose webhook endpoints to receive delivery status updates from Vonage. This solves the common problem of ""fire and forget"" SMS sending, providing visibility into the message lifecycle after it leaves the Vonage platform. We'll use the Vonage Node.js SDK for seamless API interaction and ngrok for local development testing.
System architecture
Here's a high-level overview of how the components interact:
- User/Trigger: Initiates the SMS send request to the Node.js application.
- Node.js Application (Express):
- Receives the send request.
- Uses the Vonage Node.js SDK to call the Vonage Messages API.
- Listens for incoming webhook events (status updates) from Vonage.
- Vonage Messages API:
- Accepts the SMS send request.
- Delivers the SMS message via carrier networks.
- Sends status updates (e.g.,
submitted,delivered,failed) to the configured Status Webhook URL.
- Carrier Network: Delivers the SMS to the recipient's handset.
- Recipient: Receives the SMS message.
(Note: A sequence diagram illustrating the component interactions was present here in the original document.)
Prerequisites
Before you begin, ensure you have the following:
- Node.js and npm: Installed on your system. Download from nodejs.org.
- Vonage API Account: Sign up at Vonage API Dashboard. You'll need your API Key and API Secret.
- Vonage Application: You'll create one using the Messages API capability. This provides an Application ID and allows generating a private key.
- Vonage Virtual Number: Purchase a Vonage number capable of sending SMS messages from the dashboard (Numbers > Buy Numbers).
- ngrok: A tool to expose your local server to the internet for webhook testing. Download from ngrok.com and create a free account.
- Git: (Optional) For version control and cloning the example repository.
1. Setting up the project
Let's create the project directory, initialize Node.js, and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and run:
bashmkdir vonage-sms-status-guide cd vonage-sms-status-guide -
Initialize Node.js Project: This creates a
package.jsonfile.bashnpm init -y -
Install Dependencies: We need
expressfor the web server,@vonage/server-sdkto interact with the Vonage API,dotenvto manage environment variables, andjsonwebtokento verify webhook signatures. We also recommendbody-parserfor reliable webhook verification.bash# Note: Added body-parser for reliable signature verification npm install express @vonage/server-sdk dotenv jsonwebtoken body-parser -
Create Project Files: Create the main application file and environment configuration files.
bashtouch index.js .env .env.example .gitignore -
Configure
.gitignore: Add sensitive files and directories to.gitignoreto prevent committing them to version control.text# .gitignore node_modules/ .env private.key npm-debug.log *.logExplanation: We ignore
node_modules(can be reinstalled),.env(contains secrets),private.key(Vonage private key), and log files. -
Set up Environment Variables: Create a
.env.examplefile to list the required variables. This serves as a template.dotenv# .env.example VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_SIGNATURE_SECRET # From Application settings VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # In E.164 format, e.g., +12015550123 PORT=3000 BASE_URL=http://localhost:3000 # Will be updated with ngrok URLNow, create your actual
.envfile by copying.env.exampleand filling in your real credentials.bashcp .env.example .envExplanation:
VONAGE_API_KEY,VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Obtained after creating a Vonage Application (next step).VONAGE_PRIVATE_KEY_PATH: Path to the private key file downloaded when creating the application. We assume it's in the project root namedprivate.key.VONAGE_SIGNATURE_SECRET: Found in your Vonage Application settings under 'Webhook signature'. Used to verify incoming webhooks.VONAGE_NUMBER: Your purchased Vonage virtual number capable of sending SMS (must be in E.164 format, e.g.,+12015550123).PORT: The port your local server will run on.BASE_URL: The base URL for your webhook endpoints. We'll update this later with the ngrok URL.
2. Configuring Vonage
You need a Vonage Application configured for the Messages API to handle sending and status updates.
- Navigate to Applications: Log in to the Vonage API Dashboard and go to ""Applications"" > ""Create a new application"".
- Name Your Application: Enter a descriptive name (e.g., ""Node SMS Status Guide App"").
- Generate Keys: Click ""Generate public and private key"". Immediately save the
private.keyfile that downloads. Place this file in your project root directory (matchingVONAGE_PRIVATE_KEY_PATHin.env). The public key is stored by Vonage. - Note Application ID: Copy the generated Application ID and paste it into your
.envfile forVONAGE_APPLICATION_ID. - Enable Capabilities: Find the ""Capabilities"" section.
- Toggle on ""Messages"".
- Enter placeholder URLs for now (we'll update these after starting ngrok):
- Inbound URL:
http://example.com/webhooks/inbound - Status URL:
http://example.com/webhooks/status - Why: The Status URL is where Vonage will POST delivery status updates. The Inbound URL is for receiving SMS replies (optional for this guide but good practice to configure).
- Inbound URL:
- Find Signature Secret: Scroll down to the 'Webhook signature' section within the Messages capability settings. Copy the 'Secret' and paste it into your
.envfile forVONAGE_SIGNATURE_SECRET. - Link Virtual Number: Scroll down further to ""Link virtual numbers"". Find your purchased Vonage number and click ""Link"".
- Save Changes: Click ""Save changes"" at the bottom of the page.
- (Optional but Recommended) Set Messages API as Default: Navigate to your main account ""Settings"". Scroll to ""API Settings"" > ""SMS Settings"". Ensure ""Default SMS Setting"" is set to ""Messages API"". This ensures consistency if other parts of your account interact with SMS. Save changes if necessary.
3. Setting up the webhook server (Express)
Now, let's write the Node.js code using Express to handle incoming webhooks.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const bodyParser = require('body-parser'); // Import body-parser
const jwt = require('jsonwebtoken');
const { Vonage } = require('@vonage/server-sdk');
const app = express();
// --- Middleware ---
// Capture raw body *before* JSON parsing for reliable signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
// Save the raw buffer onto the request object
req.rawBody = buf;
}
}));
// Use express.urlencoded({ extended: true }) for URL-encoded data
app.use(express.urlencoded({ extended: true }));
// --- Vonage Initialization ---
// Check for essential environment variables
if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_SIGNATURE_SECRET) {
console.error('Error: VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, or VONAGE_SIGNATURE_SECRET not set in .env');
process.exit(1);
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY, // Optional for Messages API JWT auth, but good practice
apiSecret: process.env.VONAGE_API_SECRET, // Optional for Messages API JWT auth
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
}, {
debug: true // Enable debug logging for the SDK
});
// --- Webhook Security Middleware ---
// Verifies the JWT signature on incoming Vonage webhooks using the raw body
const verifyVonageSignature = (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1]; // Extract Bearer token
if (!token) {
console.warn('Webhook received without Authorization header or token.');
return res.status(401).send('Unauthorized: Missing token');
}
// Verify the token using the raw body captured by bodyParser's verify function
// Vonage signs the raw request body, not the parsed JSON object.
if (!req.rawBody) {
console.error('Raw body buffer not available for verification. Ensure bodyParser.json() with verify is used before this middleware.');
return res.status(500).send('Internal Server Error: Cannot verify signature');
}
// jwt.verify throws if the signature is invalid
const decoded = jwt.verify(token, process.env.VONAGE_SIGNATURE_SECRET, {
algorithms: ['HS256'],
// IMPORTANT: Provide the raw body buffer for verification
// This assumes the payload is the raw body itself for JWT verification context
// Check Vonage docs if payload structure for signing differs
}); // Note: jwt.verify doesn't directly take the raw body for payload check,
// it verifies the signature against the token structure. The key is using the
// correct secret. Vonage includes payload claims *within* the signed JWT.
// Optional: Check payload application_uuid matches your app ID
// if (decoded.application_uuid !== process.env.VONAGE_APPLICATION_ID) {
// console.warn(`Webhook received for unexpected application ID: ${decoded.application_uuid}`);
// return res.status(401).send('Unauthorized: Invalid application ID');
// }
console.log('Webhook signature verified successfully.');
req.vonage_payload = decoded; // Attach decoded JWT payload if needed later
next(); // Signature is valid, proceed to the handler
} catch (error) {
console.error('Error verifying webhook signature:', error.message);
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).send('Unauthorized: Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
return res.status(401).send('Unauthorized: Invalid signature');
}
// Log other unexpected errors
console.error('Unexpected error during signature verification:', error);
return res.status(500).send('Internal Server Error');
}
};
// --- Webhook Endpoints ---
// Status Webhook: Receives delivery status updates
// Apply the verification middleware *only* to Vonage webhooks
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
// The body is already parsed by bodyParser.json()
const statusData = req.body;
console.log('--- Delivery Status Received ---');
console.log('Message UUID:', statusData.message_uuid);
console.log('Status:', statusData.status);
console.log('Timestamp:', statusData.timestamp);
if (statusData.error) {
console.error('Error Code:', statusData.error.code);
console.error('Error Reason:', statusData.error.reason);
}
console.log('Full Payload:', JSON.stringify(statusData, null, 2));
console.log('------------------------------');
// Vonage expects a 200 OK response to acknowledge receipt
// Failure to send 200 OK will cause Vonage to retry the webhook
res.status(200).send('OK');
});
// Inbound Webhook: Receives incoming SMS messages (optional for this guide)
// Apply the verification middleware here too for security
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
const inboundData = req.body;
console.log('--- Inbound SMS Received ---');
console.log('From:', inboundData.from?.number || 'Unknown');
console.log('To:', inboundData.to?.number || 'Unknown');
console.log('Text:', inboundData.message?.content?.text || 'N/A');
console.log('Full Payload:', JSON.stringify(inboundData, null, 2));
console.log('--------------------------');
res.status(200).send('OK');
});
// --- SMS Sending Function ---
async function sendSms(toNumber, text) {
console.log(`Attempting to send SMS to ${toNumber}`);
const fromNumber = process.env.VONAGE_NUMBER;
if (!fromNumber) {
console.error('Error: VONAGE_NUMBER not set in .env');
return;
}
// Validate recipient number format (strict E.164 check)
if (!/^\+\d{10,15}$/.test(toNumber)) {
console.error(`Error: Invalid recipient number format: ${toNumber}. Must use E.164 format (e.g., +12015550123).`);
// Optionally throw an error or return a failure indicator
return;
}
try {
const resp = await vonage.messages.send({
message_type: ""text"",
to: toNumber, // E.164 format (e.g., +14155550100)
from: fromNumber, // Your Vonage number
channel: ""sms"",
text: text,
// client_ref: 'my-internal-tracking-id-123' // Optional client reference
});
console.log('SMS Submitted Successfully!');
console.log('Message UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending SMS:', err?.response?.data || err.message);
// Log detailed error if available from Vonage response
if (err?.response?.data) {
console.error('Vonage Error Details:', JSON.stringify(err.response.data, null, 2));
}
}
}
// --- Example Send Endpoint (for testing) ---
app.post('/send-sms', (req, res) => {
const { to, text } = req.body;
if (!to || !text) {
return res.status(400).send('Missing ""to"" or ""text"" in request body.');
}
// Strict E.164 format check
if (!/^\+\d{10,15}$/.test(to)) {
return res.status(400).send('Invalid ""to"" number format. Use E.164 (e.g., +12015550123).');
}
sendSms(to, text); // Fire and forget for this example
res.status(202).send('SMS send request accepted.');
});
// --- Start Server ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`Webhook URLs should be configured in Vonage based on your BASE_URL:`);
console.log(`Status URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/status`);
console.log(`Inbound URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/inbound`);
// Example: Send a test SMS on startup (remove in production)
// Make sure to add a valid test number in E.164 format
// const TEST_RECIPIENT = '+14155550101'; // Replace with your test phone number
// if (process.env.NODE_ENV !== 'production' && TEST_RECIPIENT) {
// console.log(`Sending initial test SMS to ${TEST_RECIPIENT}...`);
// setTimeout(() => sendSms(TEST_RECIPIENT, 'Hello from Vonage Node Guide! Startup Test - ' + new Date().toLocaleTimeString()), 2000);
// }
});Explanation:
- We initialize Express and load
.envvariables. - The Vonage SDK is initialized using the Application ID and private key path from
.env. Debug mode is enabled for detailed SDK logs. - Webhook Security:
body-parsermiddleware with averifyfunction is used to capture the raw request body before JSON parsing. TheverifyVonageSignaturemiddleware then usesjsonwebtokenand yourVONAGE_SIGNATURE_SECRETto verify the JWT signature from theAuthorization: Bearer <token>header. This ensures the request genuinely originated from Vonage. This raw body approach is crucial for reliable production verification. /webhooks/status: This route handles POST requests from Vonage containing delivery status updates. It logs the received data and sends a200 OKresponse. This acknowledgment is vital; otherwise, Vonage will retry sending the webhook./webhooks/inbound: Handles incoming SMS messages (optional).sendSmsfunction: Encapsulates the logic for sending an SMS usingvonage.messages.send(). It takes the recipient number (validated for strict E.164 format) and text as arguments. It logs themessageUuidon successful submission./send-smsendpoint: A simple POST endpoint to trigger thesendSmsfunction for testing purposes via cURL or Postman. Includes strict E.164 validation.- The server starts listening on the specified
PORT.
4. Running and testing locally
To receive webhooks from Vonage on your local machine, you need ngrok.
-
Start ngrok: Open a new terminal window and run ngrok, pointing it to the port your Node.js app is running on (default is 3000).
bashngrok http 3000ngrok will display 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-id>.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-id>.ngrok-free.appURL. This is your public base URL. -
Update
.env: Open your.envfile and set theBASE_URLto your ngrok Forwarding URL:dotenv# .env # ... other variables BASE_URL=https://<random-id>.ngrok-free.app -
Update Vonage Application Webhooks: Go back to your application settings in the Vonage Dashboard. Update the Messages capability webhook URLs using your ngrok
BASE_URL:- Inbound URL:
https://<random-id>.ngrok-free.app/webhooks/inbound - Status URL:
https://<random-id>.ngrok-free.app/webhooks/status - Click ""Save changes"".
- Inbound URL:
-
Run the Node.js Application: In your original terminal window (where your project code is), start the server:
bashnode index.jsYou should see output indicating the server is running and listening.
-
Test Sending SMS: Open another terminal or use a tool like Postman to send a POST request to your local
/send-smsendpoint. Replace<your_test_phone>with your actual mobile number in E.164 format (e.g.,+14155550101).bashcurl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+14155550101"", ""text"": ""Testing Vonage Status Callbacks! Time: '\''$(date)'\''"" }'You should see:
- Logs in your Node.js application terminal showing the SMS submission attempt and the
messageUuid. - Shortly after, you should receive the SMS on your test phone.
- Logs in your Node.js application terminal showing the SMS submission attempt and the
-
Observe Status Webhooks: Watch your Node.js application terminal. As the message progresses through the delivery lifecycle, Vonage will send POST requests to your
/webhooks/statusendpoint. You should see logs like:Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: submitted Timestamp: 2023-10-26T10:00:05.123Z Full Payload: { ... } ------------------------------ Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: delivered Timestamp: 2023-10-26T10:00:07.456Z Full Payload: { ... } ------------------------------Common statuses include
submitted,delivered,rejected,failed,accepted(intermediate carrier status),undeliverable. -
Inspect with ngrok Web Interface: Open
http://127.0.0.1:4040in your browser. This interface shows all requests forwarded by ngrok, allowing you to inspect the exact headers (includingAuthorization) and payloads received from Vonage, which is invaluable for debugging.
5. Error handling and logging
Production applications require more robust error handling and logging.
- Webhook Handler Reliability: Wrap the logic inside your webhook handlers (
/webhooks/status,/webhooks/inbound) intry...catchblocks to prevent the server from crashing due to unexpected errors in processing the payload. Always ensure a200 OKis sent back to Vonage unless there's a signature verification failure (which returns401). - Logging: While
console.logis used in this guide for simplicity, replace it with a structured logger like Winston or Pino in production. This allows for log levels (info, warn, error), formatting (JSON), and easier integration with log management systems.- Log critical information:
message_uuid,status,timestamp, recipient/sender numbers, and any error codes/reasons. - Log errors during SMS sending (
catchblock insendSms) and webhook processing.
- Log critical information:
- Vonage Retry Mechanism: Remember that Vonage retries webhook delivery if it doesn't receive a
200 OKresponse within a short timeout (typically a few seconds). Ensure your processing is fast or happens asynchronously (e.g., push the payload to a queue) to avoid timeouts and duplicate processing from retries. Your endpoint must be idempotent if possible. - Detailed Send Errors: The
catchblock insendSmsshould inspect theerr.response.dataobject from the Vonage SDK for detailed error information provided by the API (e.g., invalid number format, insufficient funds).
// Example: Enhanced Status Webhook Handler (Conceptual)
const pino = require('pino'); // Example using Pino
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
app.post('/webhooks/status', verifyVonageSignature, async (req, res) => {
const statusData = req.body;
const logData = { // Structure your logs
message_uuid: statusData.message_uuid,
status: statusData.status,
timestamp: statusData.timestamp,
error_code: statusData.error?.code,
error_reason: statusData.error?.reason,
webhook_type: 'status'
};
logger.info(logData, 'Processing status update for message');
try {
// --- Add your business logic here ---
// Example: Update a database record based on status
// await updateMessageStatusInDB(statusData.message_uuid, statusData.status, statusData.timestamp, statusData.error);
if (statusData.error) {
logger.warn(logData, `Message status update indicates error`);
}
// ------------------------------------
res.status(200).send('OK'); // Acknowledge receipt *after* basic processing attempt
} catch (error) {
logger.error({ err: error, ...logData }, 'Error processing status webhook');
// Still send 200 OK if the error is in *your* processing logic,
// unless you specifically want Vonage to retry (use with caution).
// If the error indicates bad data you can't handle, 200 might still be appropriate
// to prevent endless retries. Log the error thoroughly.
// Consider sending 500 only for unexpected server errors preventing acknowledgement.
res.status(500).send('Internal Server Error during processing'); // Or potentially 200 OK depending on error strategy
}
});6. Security considerations
Securing your application and webhooks is vital.
- Webhook Signature Verification: Always verify the JWT signature using the
VONAGE_SIGNATURE_SECRETand the raw request body, as implemented in theverifyVonageSignaturemiddleware. This is the primary mechanism to ensure the request genuinely originated from Vonage and hasn't been tampered with. - API Credential Security:
- Never commit your
.envfile orprivate.keyto version control. Use.gitignore. - In production, use environment variables provided by your hosting platform or a dedicated secrets management service (like AWS Secrets Manager, HashiCorp Vault, etc.) instead of a
.envfile.
- Never commit your
- HTTPS: Always use HTTPS for your webhook URLs in production. ngrok provides HTTPS forwarding, and your deployment environment should also be configured for HTTPS.
- Input Validation: Sanitize and validate any user-provided input, especially if exposing the
/send-smsendpoint publicly. The E.164 check is a good start; also consider message lengths and potentially filter content. - Rate Limiting: If the
/send-smsendpoint is exposed, implement rate limiting (e.g., usingexpress-rate-limit) to prevent abuse.
7. Deployment
Deploying this application involves moving beyond ngrok.
- Choose a Hosting Platform: Options include Heroku, AWS (EC2, Lambda, Elastic Beanstalk), Google Cloud (Cloud Run, App Engine), DigitalOcean, Vercel, Render, etc.
- Environment Variables: Configure your production environment variables (
VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_APPLICATION_ID,VONAGE_SIGNATURE_SECRET,VONAGE_NUMBER,PORT,VONAGE_PRIVATE_KEY_PATH) securely through your hosting provider's interface or secrets management. Ensure theprivate.keyfile is deployed securely to the location specified byVONAGE_PRIVATE_KEY_PATH. - Stable Public URL: Your deployed application will have a stable public URL (e.g.,
https://your-app-name.herokuapp.com). Update the webhook URLs in your Vonage Application settings to use this production URL (must be HTTPS). - Procfile/Dockerfile: Depending on the platform, you might need a
Procfile(Heroku) orDockerfile(container-based deployments) to define how to start your application (node index.js). - CI/CD: Set up a Continuous Integration/Continuous Deployment pipeline (e.g., GitHub Actions, GitLab CI, Jenkins) to automate testing and deployment.
8. Troubleshooting and Caveats
- Webhook Not Received:
- Check ngrok/server logs for errors (startup, request handling, signature verification).
- Verify the Status URL in the Vonage Dashboard exactly matches your ngrok/production URL +
/webhooks/status(case-sensitive, HTTPS in production). - Ensure your server is running and accessible from the internet (ngrok status should be ""online""; check production deployment status).
- Check the Vonage Dashboard API Logs and Application event logs for errors reported by Vonage when trying to reach your webhook (e.g., 4xx/5xx responses, timeouts).
- Firewall issues might block incoming requests to your server.
- Invalid Signature (401 Unauthorized):
- Double-check
VONAGE_SIGNATURE_SECRETin your environment variables matches the secret in the Vonage Application settings exactly (no extra spaces, etc.). - Confirm that
bodyParser.json({ verify: ... })is correctly capturing the raw body (req.rawBody) before theverifyVonageSignaturemiddleware runs. Use the ngrok web interface (http://127.0.0.1:4040) to inspect theAuthorizationheader on incoming requests. - Ensure the system clocks on your server and Vonage's servers are reasonably synchronized (JWTs have expiration times).
- Double-check
- SMS Not Sending:
- Check Node.js logs for errors from the
sendSmsfunction. Look closely aterr.response.datafor specific Vonage error codes and descriptions. - Verify API Key/Secret/Application ID/Private Key path are correct in your environment variables.
- Ensure your Vonage account has sufficient credit.
- Check if the destination number (
to) is valid and strictly in E.164 format (e.g.,+14155550101). - Confirm the Vonage number (
from) linked to the Application is SMS-capable for the destination country and correctly formatted in E.164. - Trial Account Limitations: New Vonage accounts might have restrictions (e.g., only sending to verified numbers listed in the dashboard) until topped up or fully verified.
- Check Node.js logs for errors from the
- Delayed Status Updates: Delivery receipts depend on downstream carriers; delays are possible. Not all carriers provide timely or reliable DLRs.
submittedoracceptedstatuses usually appear quickly, butdeliveredorfailedcan take longer or sometimes not arrive at all for certain destinations/networks. - Payload Variations: While the general structure is consistent, webhook payloads might occasionally have minor variations or new fields added by Vonage. Rely on documented core fields like
message_uuid,status,timestamp. Refer to the official Vonage Messages API documentation for the definitive schemas.
9. Verification checklist
Before considering the implementation complete, verify the following:
- Vonage account created and API credentials noted.
- Vonage Application created with Messages capability enabled.
- Private key downloaded, stored securely, and path correctly set in environment variables.
- Application ID and Signature Secret correctly set in environment variables.
- Vonage SMS-capable number purchased, correctly formatted (E.164), and linked to the Application.
- Status and Inbound webhook URLs correctly configured in Vonage Application settings (pointing to ngrok or production HTTPS URL).
- Node.js project initialized, dependencies installed (
express,@vonage/server-sdk,dotenv,jsonwebtoken,body-parser). -
.envfile created (or environment variables set) with all necessary variables filled. -
index.jscontains Express server setup, Vonage SDK initialization,bodyParserwith raw body capture,verifyVonageSignaturemiddleware, and webhook handlers (/webhooks/status). -
verifyVonageSignaturemiddleware is applied to webhook routes and correctly uses the raw body for verification. - Webhook handlers log incoming data and return
200 OKupon successful processing acknowledgment. -
sendSmsfunction correctly usesvonage.messages.sendand validates thetonumber format (strict E.164). - Application runs locally without errors (
node index.js). - ngrok (or production deployment) correctly forwards requests to the Node.js application.
- Sending a test SMS via the
/send-smsendpoint (or other trigger) succeeds (check logs). - Test SMS is received on the target device.
- Status webhook events (
submitted,delivered, etc.) are received and logged by the/webhooks/statusendpoint.
Frequently Asked Questions
How to track SMS delivery status with Vonage?
Use Vonage's Messages API and webhooks. Set up a webhook endpoint in your application to receive real-time status updates like 'submitted,' 'delivered,' or 'failed' from Vonage after sending an SMS message via their API.
What is the Vonage Messages API?
It's an API that lets you send and receive messages, including SMS, and track their delivery status. You integrate it into your application using the Vonage Node.js SDK or other language-specific libraries. It's more robust than 'fire and forget' methods.
Why does Vonage use webhooks for SMS status?
Webhooks provide real-time delivery updates pushed directly to your application. This is more efficient than constantly polling the API and lets you react immediately to changes in message status, building more reliable systems.
When should I use the Vonage Messages API?
Whenever your application needs to send SMS messages reliably and track their delivery status in real time. It's especially important for time-sensitive communications and two-factor authentication.
How to set up a Node.js server for Vonage SMS?
Use Express.js to create a server and the Vonage Node.js SDK to interact with the Messages API. You'll also need environment variables for your API credentials and webhook URLs. Ngrok helps during development.
What is a Vonage Application ID?
A unique identifier for your application within the Vonage platform. It's required for using the Messages API. You create an application in the Vonage Dashboard and note its ID for configuration.
What is the purpose of ngrok in Vonage webhook setup?
Ngrok creates a public, secure tunnel to your local development server, allowing Vonage to send webhooks to your machine during testing. This is necessary because local servers aren't publicly accessible on the internet.
Why use JWT signature verification for Vonage webhooks?
It guarantees the integrity and authenticity of the webhook requests. By using a shared secret, you confirm requests genuinely originated from Vonage and haven't been tampered with. This is crucial for security.
How to verify Vonage webhook signature in Node.js?
Use the 'jsonwebtoken' library and the Vonage Signature Secret from your application dashboard. Importantly, use the *raw* request body, not the parsed JSON, when verifying the signature against the token from the 'Authorization' header.
What are common Vonage SMS delivery statuses?
Typical statuses include 'submitted' (sent to Vonage), 'delivered' (reached handset), 'failed,' 'rejected' (by the carrier), 'accepted' (intermediate carrier status), and 'undeliverable.' Not all statuses are guaranteed due to carrier limitations.
How to handle Vonage webhook errors in Node.js?
Wrap your webhook handler logic in 'try...catch' blocks to handle errors gracefully. Always acknowledge receipt with a 200 OK response, even if your internal processing fails, to prevent Vonage retries. Log errors thoroughly using a structured logger.
What should I do if Vonage webhooks are not received?
Check server and ngrok logs, verify the webhook URL in your Vonage application settings matches your server's URL, and check for firewall issues. Inspect the Vonage API logs for errors reported by Vonage while trying to reach your webhook.
Why is it important to capture the raw body for webhook verification?
Vonage uses the raw, unaltered request body to generate the JWT signature, not the parsed JSON. Therefore, you must capture the raw body using middleware like `body-parser` *before* any JSON parsing occurs to ensure accurate signature verification.
What's the correct E.164 number format for Vonage?
Use the + sign followed by the country code and phone number without any spaces or special characters. Example: +14155550101. Validate user-provided numbers strictly to avoid errors.