Vonage (Nexmo) SMS Delivery Status & Callbacks with Node.js Express
Build production-ready SMS applications with Vonage Messages API, Express, and Node.js. Send SMS, receive delivery status callbacks, and handle inbound messages with webhooks.
Note: This filename references "messagebird" and "next-js" but the content covers Vonage (formerly Nexmo) with Express framework. The technical content and code examples are accurate for Vonage Messages API.
Last Updated: October 5, 2025 | Vonage Messages API Status: General Availability | SDK Version: @vonage/server-sdk v3.x+
Build a production-ready Node.js application with Express to send SMS messages, receive inbound SMS, and handle delivery status updates (callbacks) via the Vonage Messages API. This guide covers everything from project setup and core implementation to error handling, security, deployment, and testing.
Create a Node.js application using Express that can:
Send SMS messages programmatically via the Vonage Messages API
Receive incoming SMS messages sent to your Vonage virtual number via webhooks
Track real-time delivery status updates for sent SMS messages via webhooks
Why Build This:
Create reliable, two-way SMS communication in your Node.js environment. Send messages, confirm their delivery status, and react to user messages – enabling interactive SMS applications, notification systems with delivery confirmation, and more.
Technologies You'll Use:
Node.js: JavaScript runtime for building scalable network applications, ideal for I/O operations like API calls and webhooks
Express: Minimal, flexible Node.js framework with robust features for web and mobile applications – perfect for handling webhook requests
Vonage Messages API: Unified API for sending and receiving messages across SMS, MMS, WhatsApp, and more – focus on SMS with robust delivery status tracking
Vonage Node SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs in Node.js
ngrok (Development Only): Exposes your local development server to the public internet – necessary for Vonage webhooks to reach your machine during development
dotenv: Loads environment variables from a .env file into process.env – keeps sensitive credentials out of source code
System Architecture:
graph LR
subgraph Your Application
direction LR
A[Node.js/Express App]
W_In[Inbound Webhook (/webhooks/inbound)]
W_Stat[Status Webhook (/webhooks/status)]
DB[(Database - Optional)]
end
subgraph Vonage Cloud
direction LR
VAPI[Messages API]
VNum[Vonage Virtual Number]
end
User[End User Phone] -- Sends SMS --> VNum
VNum -- Relays Inbound SMS --> VAPI
VAPI -- POST --> W_In
A -- Send SMS Request --> VAPI
VAPI -- Sends SMS --> Carrier[Carrier Network]
Carrier -- Delivers SMS --> User
Carrier -- Sends DLR --> VAPI
VAPI -- POST Status --> W_Stat
A -- Optional --> DB{Store/Read Data}
W_In -- Optional --> DB
W_Stat -- Optional --> DB
What You'll Have at the End:
A functional Node.js Express application that sends SMS, receives SMS, and tracks delivery statuses – structured with security, error handling, and deployment best practices.
Before You Start:
Vonage API Account: Sign up free at Vonage Dashboard – you get free credit to start
Node.js & npm (or yarn): Install Node.js (includes npm) from nodejs.org – LTS version 18.x or higher recommended (as of October 2025)
Vonage Virtual Number: Purchase an SMS-capable number from your Vonage Dashboard (Numbers > Buy Numbers)
ngrok: Download and set up from ngrok.com – free account is sufficient. Authenticate it: ngrok config add-authtoken YOUR_TOKEN
Basic knowledge of JavaScript, Node.js, Express, and REST APIs
1. Set Up Your Project
Initialize your Node.js project and install the necessary dependencies.
Create Your Project Directory:
Open your terminal and create a new directory for your project.
Bash
mkdir vonage-sms-app
cd vonage-sms-app
Initialize Your Node.js Project:
Initialize with npm (or yarn). The -y flag accepts default settings.
Bash
npm init -y
This creates a package.json file.
Install Your Dependencies:
Install Express for the web server, the Vonage Node SDK for API interaction, and dotenv for environment variable management. Install v3.x or higher of @vonage/server-sdk for Messages API support.
Bash
npm install express @vonage/server-sdk dotenv
Current Versions (October 2025):
express: v4.18.x or higher
@vonage/server-sdk: v3.15.x or higher
dotenv: v16.x or higher
Create Your Project Structure:
Build a basic structure for clarity.
Define Your Environment Variables (.env.example):
List the variables you need. Copy this content into both .env and .env.example. Fill .env with actual values later.
.env
# .env.example
# Vonage Application Credentials - Generated when creating a Vonage Application
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key
# Vonage Number - Your purchased virtual number in E.164 format
VONAGE_NUMBER=YOUR_VONAGE_NUMBER
# Application Port
APP_PORT=3000
# Ngrok URL (for development reference - set dynamically or manually when running)
# NGROK_BASE_URL=https://your-ngrok-subdomain.ngrok.io
Why Use .env? Store credentials and configuration separate from code – crucial for security and flexibility across environments (development, staging, production).
2. Configure Vonage for SMS and Webhooks
Set up your Vonage account and application to enable SMS sending and receiving with callbacks.
Log in to Your Vonage Dashboard: Access your account at dashboard.nexmo.com.
Set Your Default SMS API to "Messages API":
Navigate to API Settings in the left menu
Scroll to SMS settings
Set Default SMS provider to Messages API
Click Save changes
Why Messages API? Provides a unified interface across SMS, WhatsApp, and MMS with consistent webhook payloads and modern authentication (JWT-based) – the current standard as of October 2025
Create Your Vonage Application:
Vonage Applications contain your communication configurations, including keys and webhooks.
Navigate to Applications > Create a new application
Enter an Application name (e.g., "Node Express SMS App")
Click Generate public and private key – this downloads the private.key file. Save this in your project root, replacing the placeholder private.key file. Vonage stores the public key. These keys enable JWT authentication for Messages API requests.
Enable the Messages capability by toggling it on
Two fields appear: Inbound URL and Status URL – you need public URLs for these (that's where ngrok comes in)
Start ngrok:
Open a new terminal window in your project directory and start ngrok, forwarding traffic to the port your Express app will use (defaulting to 3000 from .env.example).
Bash
ngrok http 3000
ngrok provides Forwarding URLs (http and https). Copy the https URL – looks like https://random-subdomain.ngrok.io.
Configure Your Webhook URLs:
Return to your Vonage Application configuration page
Paste your ngrokhttps URL into the webhook fields, appending specific paths:
Inbound URL:YOUR_NGROK_HTTPS_URL/webhooks/inbound
Status URL:YOUR_NGROK_HTTPS_URL/webhooks/status
Why HTTPS? Always use HTTPS for webhook URLs – encrypts data in transit
Click Generate application
Copy Your Application ID:
After creation, you'll see the application details page. Copy the Application ID – you need this for your .env file.
Link Your Vonage Number:
On the application details page, scroll to Link virtual numbers
Find your purchased Vonage virtual number and click Link – tells Vonage to send events for this number (like incoming SMS) to your application's webhooks
Update Your .env File:
Open your .env file (the one without.example) and fill in the values you obtained:
.env
# .env (Fill with your actual values)
VONAGE_APPLICATION_ID=PASTE_YOUR_APPLICATION_ID_HERE
VONAGE_PRIVATE_KEY_PATH=./private.key
VONAGE_NUMBER=PASTE_YOUR_VONAGE_NUMBER_HERE # e.g., 14155550100
APP_PORT=3000
Security:.env contains secrets. Keep it in your .gitignore and never commit to version control. Use environment variable management from your deployment platform in production.
3. Write Your Core SMS Functionality
Build the Node.js/Express code to send and receive SMS.
File: src/app.js
JavaScript
// src/app.js// 1. Import Dependenciesrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const { Vonage } = require('@vonage/server-sdk'); // v3.x+ syntaxconst path = require('path'); // Needed for constructing the private key path// 2. Initialize Express Appconst app = express();
app.use(express.json()); // Middleware to parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded request bodies// 3. Initialize Vonage Client// Ensure required environment variables are presentif (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) {
console.error('ERROR: Set VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in your .env file');
process.exit(1);
}
if (!process.env.VONAGE_NUMBER) {
console.error('ERROR: Set VONAGE_NUMBER in your .env file');
process.exit(1);
}
const privateKeyPath = path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH);
let vonage;
try {
vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKeyPath // Provide the absolute path to the key file
});
console.log("Vonage client initialized successfully.");
} catch (error) {
console.error("Error initializing Vonage client:", error);
process.exit(1);
}
// 4. Define Route for Sending SMS (Example)// In production, **protect this endpoint** (e.g., API key check, user session authentication, rate limiting). Don't expose it publicly.
app.post('/send-sms', async (req, res) => {
const { to, text } = req.body;
// Basic input validationif (!to || !text) {
return res.status(400).json({
error: 'Missing required fields',
message: 'Include both "to" (recipient number) and "text" (message content) in your request body'
});
}
if (!process.env.VONAGE_NUMBER) {
return res.status(500).json({
error: 'Configuration error',
message: 'Your Vonage number is missing from the configuration. Add VONAGE_NUMBER to your .env file.'
});
}
console.log(`Attempting to send SMS from ${process.env.VONAGE_NUMBER} to ${to}`);
try {
const resp = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: to, // E.164 format (e.g., 14155550101)from: process.env.VONAGE_NUMBER, // Your Vonage numbertext: text
});
console.log('Message sent successfully:', resp);
// resp contains { message_uuid: '...' } on success
res.status(200).json({ message_uuid: resp.message_uuid, status: 'Message submitted' });
} catch (err) {
console.error('Error sending SMS:', err);
// Provide more context if availableconst statusCode = err.response?.status || 500;
const errorMessage = err.response?.data?.title || err.message || 'Failed to send SMS';
const errorDetails = {
error: errorMessage,
details: err.response?.data,
help: statusCode === 401 ? 'Check your VONAGE_APPLICATION_ID and private.key file' :
statusCode === 422 ? 'Verify your "to" number is in E.164 format (e.g., 14155550101)' :
statusCode === 429 ? 'You have exceeded the rate limit. Wait before retrying.' :
'Check the details field for more information'
};
res.status(statusCode).json(errorDetails);
}
});
// 5. Define Webhook Endpoint for Inbound SMS
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound SMS Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// TODO: Process the inbound message (req.body)// Example: Store in DB, trigger a response, etc.// Key fields: req.body.from, req.body.to, req.body.text, req.body.message_uuid, req.body.timestamp// Acknowledge receipt to Vonage immediately
res.status(200).end();
// Why 200? Vonage expects a 200 OK response. Without one// within the timeout period, Vonage assumes failure and retries the webhook,// potentially causing duplicate processing.
});
// 6. Define Webhook Endpoint for Delivery Status Updates
app.post('/webhooks/status', (req, res) => {
console.log('--- Delivery Status Update Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// TODO: Process the status update (req.body)// Example: Update message status in DB using req.body.message_uuid// Key fields: req.body.message_uuid, req.body.status ('delivered', 'failed', 'rejected', 'accepted', 'buffered'),// req.body.timestamp, req.body.error (if failed/rejected)// Acknowledge receipt to Vonage
res.status(200).end();
});
// 7. Basic Health Check Endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK', timestamp: newDate().toISOString() });
});
// 8. Define Port and Start Server (Only if run directly)const port = process.env.APP_PORT || 3000;
if (require.main === module) { // Check if the script is being run directly
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
// Remind user about ngrok if not in production environmentif (process.env.NODE_ENV !== 'production') {
console.log(`Ensure ngrok is running and forwarding to this port.`);
console.log(`Configure these webhook URLs in your Vonage Application:`);
console.log(` Inbound: YOUR_NGROK_HTTPS_URL/webhooks/inbound`);
console.log(` Status: YOUR_NGROK_HTTPS_URL/webhooks/status`);
}
});
}
// 9. Basic Error Handling Middleware (Optional but Recommended)
app.use((err, req, res, next) => {
console.error('Unhandled Error:', err.stack || err);
res.status(500).json({
error: 'Internal server error',
message: 'Something went wrong. Check the server logs for details.',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// 10. Export the app for testingmodule.exports = app;
Express Init: Creates the Express app and adds middleware to parse incoming JSON and URL-encoded request bodies – crucial for handling webhook payloads
Vonage Init: Initializes the Vonage client using applicationId and privateKey path from environment variables. Includes error checking for missing variables. Uses path.resolve to ensure the path to private.key is correct regardless of where you run the script
/send-sms Route: A POST endpoint to send SMS. Expects to (recipient number) and text (message content) in the JSON body. Validates input and calls vonage.messages.send() using the sms channel. Returns success/error responses with helpful messages. Includes a warning about production protection
/webhooks/inbound Route: A POST endpoint matching your Inbound URL in the Vonage Application. Vonage sends data here when your virtual number receives SMS. Logs the payload (req.body), includes a TODO for your logic, and immediately sends 200 OK. Send the 200 OK quickly before heavy processing.
/webhooks/status Route: A POST endpoint matching your Status URL. Vonage sends delivery status updates here for messages you sent. Logs the payload, includes a TODO, and sends 200 OK
/health Route: Simple endpoint for monitoring systems to check if your application is running
Server Start: Reads the port from APP_PORT (defaulting to 3000) and starts the Express server only if you run the script directly (using if (require.main === module)). Includes helpful reminders about ngrok configuration during development
Error Handler: Basic Express error-handling middleware to catch unhandled errors in route handlers
Export: Exports the app instance for testing frameworks like Supertest
4. Run and Test Locally
Ensure ngrok is running (from Step 2.4) and forwarding to port 3000. Verify the HTTPS URL matches the one in your Vonage Application webhook settings.
Verify your .env file contains your Vonage Application ID, number, and the correct path to private.key.
Start Your Node.js Application:
In the terminal where you created the project (not the ngrok one), run:
Bash
node src/app.js
You should see "Server listening at http://localhost:3000" and the ngrok reminder messages.
Test Sending SMS:
Use curl or Postman to send a POST request to your local /send-sms endpoint. Replace YOUR_PHONE_NUMBER with your mobile number in E.164 format (e.g., 14155550101).
Bash
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-d '{
"to": "YOUR_PHONE_NUMBER",
"text": "Hello from Node.js Vonage App!"
}'
Expected Result:
Your terminal running node src/app.js logs "Attempting to send SMS…" then "Message sent successfully: …" with a message_uuid
The curl command receives JSON: {"message_uuid":"...","status":"Message submitted"}
You receive the SMS on your phone shortly
Test Receiving Status Updates:
Wait a few moments after sending the SMS
Expected Result:
Your terminal running node src/app.js logs "--- Delivery Status Update Received ---" followed by the JSON payload from Vonage. Look for the message_uuid matching your sent message and a status field (e.g., delivered, accepted, buffered) – exact status depends on carrier support
Test Receiving Inbound SMS:
From your mobile phone, send an SMS to your Vonage virtual number
Expected Result:
Your terminal running node src/app.js logs "--- Inbound SMS Received ---" followed by the JSON payload containing message details (from, text, etc.)
5. Implement Error Handling, Logging, and Retries
Build production-ready error handling and logging.
Best Practices (October 2025):
Error Handling:
Vonage SDK Errors: The try...catch block around vonage.messages.send() handles specific API call errors. Inspect the err object (especially err.response.data or err.message) for details from the Vonage API
Common Error Codes:
401 (authentication failed) – Check your VONAGE_APPLICATION_ID and private.key file
403 (forbidden) – Verify permissions and account status
422 (validation error) – Ensure phone numbers are in E.164 format
500 (server error) – Retry with exponential backoff
Webhook Errors: Wrap logic inside your webhook handlers (/webhooks/inbound, /webhooks/status) in try...catch blocks. Log errors but still send a 200 OK response to Vonage to prevent retries (unless the request is fundamentally invalid)
Unhandled Errors: The Express error handler (app.use((err, req, res, next) => {...})) catches errors not caught in specific routes
Logging:
Use pino (recommended for performance) or winston in production instead of console.log
Benefits: Structured logging (JSON format), configurable log levels (debug, info, warn, error), ability to write to files or external services (Datadog, Logstash)
What to Log: Key events (app start, SMS sent/received, status updates), errors with stack traces, important request details (message_uuid), configuration issues
Security: Never log sensitive data like full phone numbers (mask them) or private keys
Retries:
Vonage Webhook Retries: Vonage automatically retries webhooks if they don't receive 200 OK within ~30 seconds. Respond quickly with 200 OK
Retry Schedule: Vonage uses exponential backoff (1s, 5s, 15s, 60s) for up to 24 hours
Application-Level Retries (Sending): If sending SMS fails due to temporary issues (network hiccup, 5xx error from Vonage), implement retry strategy with exponential backoff. Use libraries like async-retry
Application-Level Retries (Webhook Processing): If processing within a webhook handler fails after sending 200 OK (e.g., database write fails), log the error and use a background job queue (BullMQ, Kue, RabbitMQ) to retry the processing task independently
6. Add Database Integration (Optional but Recommended)
Integrate a database for persistence and tracking.
Mongoose (MongoDB): De-facto standard for MongoDB with Node.js
Design Your Schema:
Create a messages table:
message_uuid (VARCHAR/TEXT, PRIMARY KEY/UNIQUE) - From Vonage response/webhooks
direction (ENUM/VARCHAR: 'outbound', 'inbound')
to_number (VARCHAR)
from_number (VARCHAR)
body (TEXT)
status (VARCHAR, e.g., 'submitted', 'delivered', 'failed', 'read') - Updated via status webhook
vonage_timestamp (TIMESTAMP WITH TIME ZONE) - From webhook payload
error_code (VARCHAR, nullable) - From status webhook if failed/rejected
created_at (TIMESTAMP WITH TIME ZONE)
updated_at (TIMESTAMP WITH TIME ZONE)
Implement Your Database Logic:
Sending: After successfully calling vonage.messages.send(), insert a record with message_uuid, direction='outbound', to, from, text, status='submitted', and timestamps
Status Webhook: Find the message by req.body.message_uuid and update its status, vonage_timestamp, error_code (if present), and updated_at. Handle cases where the message_uuid might not be found (log an error)
Inbound Webhook: Insert a new record with direction='inbound', details from req.body, status='received', and timestamps
7. Secure Your Application
Protect your application and user data.
Secure Your Webhooks:
Webhook URLs are public. The Vonage Messages API uses JWT-based authentication when you send messages, but securing incoming webhook endpoints requires standard practices:
HTTPS: Essential – encrypts data in transit. Always use https URLs for webhooks (required by Vonage as of October 2025)
Endpoint Secrecy: Avoid overly simple webhook paths. The paths /webhooks/inbound and /webhooks/status are reasonably specific
Verify Source (Advanced): Check the source IP address of incoming webhook requests against published Vonage IP ranges (available in Vonage documentation, but subject to change)
JWT Verification (Advanced): While Messages API webhooks don't include signed JWTs by default (as of October 2025), implement custom verification headers if needed
Manage Your Secrets:
Use environment variables (.env locally, platform-provided secrets in production). Don't commit .env or private.key
Rotate keys periodically if your security policy requires it
Validate Your Input:
Validate payloads for your endpoints (like /send-sms). Ensure required fields are present, types are correct, and formats match (e.g., E.164 for phone numbers). Use libraries like express-validator or joi
Sanitize input if you store or display user-provided text (like inbound SMS content) to prevent XSS attacks. Use proper database parameterization or libraries like dompurify (if rendering in HTML)
Implement Rate Limiting:
Protect public-facing endpoints (especially /send-sms if exposed) from abuse. Use middleware like express-rate-limit
Use HTTPS:
Always run your production application behind HTTPS. ngrok provides this locally. Use a load balancer or reverse proxy (Nginx, Caddy, or platform services like AWS ALB/Cloudflare) to handle SSL termination in production
8. Handle Special SMS Cases
Address real-world SMS nuances.
Format Your Numbers:
Always use E.164 format (e.g., +14155550101 or 14155550101 without spaces) when sending to numbers to Vonage. Vonage provides from numbers in webhooks in similar format
Vonage automatically handles standard opt-out keywords (STOP, UNSUBSCRIBE, QUIT) for US/Canada long codes by default
Configure this in Settings > SMS in the Vonage Dashboard
Respect opt-outs and maintain an opt-out list in your application
Interpret Delivery Statuses:
Not all carriers reliably provide delivered status updates (DLRs – Delivery Receipt Reports)
Common statuses: submitted, accepted, buffered, delivered, failed, rejected, expired
Design your logic to handle missing or delayed DLRs
failed or rejected statuses reliably indicate non-delivery
Manage Time Zones:
Timestamps in Vonage webhooks (timestamp) are in UTC (ISO 8601 format: YYYY-MM-DDTHH:MM:SS.sssZ)
Store timestamps in UTC in your database and convert to local time zones only for display
9. Optimize Your Performance
Ensure your webhook handlers are fast and efficient.
Respond Quickly:
Send your 200 OK response within 30 seconds (Vonage timeout). Aim for <1 second response time – this is your most critical optimization
Process in the Background:
Recommended Solutions: BullMQ (Redis-based, modern), Celery (if using Python workers), AWS SQS, Google Cloud Tasks
Pattern: Webhook receives data → Validates → Queues job → Returns 200 OK → Worker processes job asynchronously
Index Your Database:
Index columns used for lookups:
message_uuid (PRIMARY KEY or UNIQUE index)
status (if you query by status frequently)
created_at (for time-range queries)
Cache Frequently Accessed Data:
Use Redis or in-memory caching (node-cache) for frequently accessed data
10. Monitor Your Application
Understand how your application performs and diagnose issues.
Implement Health Checks:
The /health endpoint is your starting point. Production monitoring systems (AWS CloudWatch, Datadog, Prometheus/Grafana, UptimeRobot) can ping this endpoint to verify your app is live
Track Your Metrics:
Monitor these key performance indicators (KPIs):
Webhook request rate (inbound, status)
Webhook response times (should be very low)
SMS sending rate
API call latency (Vonage send calls)
Error rates (webhook failures, API call failures)
Queue sizes (if using background jobs)
Use libraries like prom-client for Prometheus or integrate with APM tools (Datadog APM, New Relic)
Track Your Errors:
Integrate services like Sentry or Bugsnag to capture, aggregate, and alert on application errors in real-time
Aggregate Your Logs:
Ship logs to a centralized platform (ELK stack, Datadog Logs, Splunk) for easier searching and analysis
Create Your Dashboards:
Visualize key metrics and log trends using Grafana, Kibana, or your APM provider's dashboarding features
11. Troubleshoot Common Issues
Solve common problems and their solutions.
Webhooks Not Reaching Your App:
ngrok: Is ngrok running? Free URLs expire after 2-8 hours – restart ngrok and update Vonage Application webhook URLs
Vonage Config: Verify webhook URLs in Application settings match your ngrok HTTPS URL + exact paths
HTTPS Required: Vonage requires HTTPS for webhooks (as of October 2025) – HTTP URLs are rejected
Firewall: If deployed, allow incoming traffic from Vonage IP ranges
App Running: Check application logs for startup errors
Your App Receives Webhooks but Errors Occur:
Check Logs: Look for errors in your Node.js application console output or log files
Payload Issues: Are you parsing the req.body correctly? Ensure express.json() middleware is used
Missing 200 OK: Does your handler send res.status(200).end() reliably and quickly? Check the Vonage Dashboard (Logs section) for webhook retry attempts
SMS Sending Fails:
Credentials: Double-check VONAGE_APPLICATION_ID and the path/content of VONAGE_PRIVATE_KEY_PATH in .env
Vonage Balance: Do you have credit in your Vonage account?
'To' Number: Is the recipient number valid and in E.164 format?
'From' Number: Is VONAGE_NUMBER correctly set in .env and linked to the Vonage Application used for initialization?
API Errors: Log the err object from the catch block in /send-sms – err.response.data often contains detailed error codes and messages from Vonage
No Delivery Status Updates (/webhooks/status not hit):
Status URL Config: Verify the Status URL is correctly set in your Vonage Application
Carrier Support: DLRs are carrier-dependent – not all networks/countries provide reliable delivery receipts
Number Type: Some number types (e.g., short codes) might have different DLR behavior
Messages API vs. Legacy SMS API:
Critical: Use the Messages API consistently (not the legacy SMS API)
SDK method: vonage.messages.send() (Messages API) vs. vonage.sms.send() (legacy SMS API)
Webhook structure differs between APIs
Set "Default SMS provider" to "Messages API" in Dashboard > API Settings
Application-specific webhooks (used here) are for Messages API – account-level webhooks (in main Settings) are for legacy SMS API
12. Deploy Your Application
Move from local development to production.
Choose Your Deployment Platform (October 2025):
Serverless/PaaS Options:
Vercel (Edge Functions): Excellent for Next.js, supports Node.js functions
AWS Lambda + API Gateway: Serverless, pay-per-request pricing
Google Cloud Run: Containerized apps, auto-scaling
Heroku: Simple PaaS for prototypes (note: free tier deprecated in late 2022)
Option 3: For serverless: Include private.key in deployment bundle (ensure it's in .gitignore)
Never commit private.key to version control
Run Your Build Process: If using TypeScript or a build step, ensure it runs before deployment
Manage Your Process: Use a process manager like pm2 or rely on the platform's built-in service management (e.g., systemd, Heroku dynos) to keep your Node.js app running, handle restarts, and manage logs
Configure HTTPS: Ensure your deployment platform handles HTTPS termination (most PaaS/Serverless platforms do) or configure a reverse proxy (Nginx, Caddy) if deploying to a VM
Set Up Your CI/CD Pipeline:
Modern CI/CD tools:
GitHub Actions: Built into GitHub, excellent for open source
GitLab CI/CD: Integrated with GitLab repositories
CircleCI: Popular third-party CI/CD
Jenkins: Self-hosted, highly customizable
Typical Pipeline: Lint → Unit Tests → Integration Tests → Build → Deploy to Staging → E2E Tests → Deploy to Production
Frequently asked questions
Use the Vonage Messages API and Node.js SDK. After setting up a Vonage application and linking a virtual number, you can send SMS messages programmatically via the /send-sms endpoint, providing the recipient's number and the message text in the request body. The Vonage SDK simplifies the API interaction, handling authentication and request formatting.
The Vonage Messages API is a unified platform for sending and receiving messages across multiple channels, including SMS, MMS, WhatsApp, and more. This guide focuses on using the API for sending SMS messages and receiving delivery status updates, enabling reliable two-way communication.
Ngrok creates a secure tunnel from your local development environment to the public internet. This allows Vonage's webhooks (inbound SMS and delivery receipts) to reach your local server during development, as these webhooks require a publicly accessible URL.
Database integration is highly recommended for production applications. A database (like PostgreSQL or MongoDB) helps persist message data (inbound, outbound, delivery statuses), enabling tracking, analytics, and features like message history or user opt-out management.
Yes, you can receive inbound SMS messages to your Vonage virtual number. By configuring the inbound webhook URL in your Vonage Application settings, Vonage will forward incoming messages to your application as POST requests, allowing you to process and respond to them.
Vonage sends delivery status updates (DLRs) via webhooks to the Status URL configured in your Vonage Application. The payload contains the message_uuid and status (e.g., 'delivered', 'failed'). You can use this information to update message status in your database and implement logic based on delivery outcomes.
A Vonage Application acts as a container for your communication configuration, including API keys, webhook URLs, and linked virtual numbers. It centralizes the settings for your project and simplifies integration with Vonage services.
Key security measures include using HTTPS for all communication, securing your .env file containing API credentials, validating input to prevent vulnerabilities, and implementing rate limiting on public endpoints to protect against abuse.
Implement try...catch blocks around vonage.messages.send() to handle API errors. Inspect the err object for details. For temporary failures (e.g., network issues), consider implementing retry logic with exponential backoff using a library like async-retry.
Log key events (app start, SMS sent/received, status updates), errors with stack traces, message_uuid for tracking, and any configuration issues. Use a dedicated logging library (like pino or winston) for structured logging and advanced features.
Verify ngrok is running (if in development) and forwarding to the correct port. Ensure the webhook URLs in your Vonage Application settings match the ngrok URL and paths (/webhooks/inbound, /webhooks/status). Check your firewall and application logs for errors.
Confirm the Status URL is correctly set in your Vonage Application. Note that delivery receipts (DLRs) are carrier-dependent and not always reliable. Some networks/countries might not provide delivered status. The message might still be delivered even if the status is not 'delivered'.
E.164 is an international telephone number format (e.g., +14155550101). Always use this format for recipient numbers (to) when sending SMS with Vonage. This ensures correct routing and avoids issues with regional number variations.
Vonage handles concatenation of long messages exceeding the standard SMS character limit. Be aware that longer messages are split into segments, and you are billed per segment. Consider message length and costs when designing your application.
Note: This filename references "messagebird" and "next-js" but the content covers Vonage (formerly Nexmo) with Express framework. The technical content and code examples are accurate for Vonage Messages API.
Last Updated: October 5, 2025 | Vonage Messages API Status: General Availability | SDK Version: @vonage/server-sdk v3.x+
Build a production-ready Node.js application with Express to send SMS messages, receive inbound SMS, and handle delivery status updates (callbacks) via the Vonage Messages API. This guide covers everything from project setup and core implementation to error handling, security, deployment, and testing.
Technologies: Node.js, Express, Vonage Messages API, Vonage Node SDK (@vonage/server-sdk v3.x+), ngrok (for development)
Official Documentation: Vonage Messages API | Vonage Node SDK
What You'll Build
Create a Node.js application using Express that can:
Why Build This: Create reliable, two-way SMS communication in your Node.js environment. Send messages, confirm their delivery status, and react to user messages – enabling interactive SMS applications, notification systems with delivery confirmation, and more.
Technologies You'll Use:
@vonage/server-sdk): Simplifies interaction with Vonage APIs in Node.js.envfile intoprocess.env– keeps sensitive credentials out of source codeSystem Architecture:
What You'll Have at the End: A functional Node.js Express application that sends SMS, receives SMS, and tracks delivery statuses – structured with security, error handling, and deployment best practices.
Before You Start:
ngrok config add-authtoken YOUR_TOKEN1. Set Up Your Project
Initialize your Node.js project and install the necessary dependencies.
Create Your Project Directory: Open your terminal and create a new directory for your project.
mkdir vonage-sms-app cd vonage-sms-appInitialize Your Node.js Project: Initialize with npm (or yarn). The
-yflag accepts default settings.This creates a
package.jsonfile.Install Your Dependencies: Install Express for the web server, the Vonage Node SDK for API interaction, and
dotenvfor environment variable management. Install v3.x or higher of @vonage/server-sdk for Messages API support.Current Versions (October 2025):
express: v4.18.x or higher@vonage/server-sdk: v3.15.x or higherdotenv: v16.x or higherCreate Your Project Structure: Build a basic structure for clarity.
mkdir src config touch src/app.js .env .env.example .gitignore private.keysrc/app.js: Your main application codeconfig/: (Optional) For complex configurations later.env: Stores sensitive credentials (API keys, etc.) – never commit this file.env.example: Template showing required environment variables – commit this file.gitignore: Specifies files/directories Git should ignoreprivate.key: Placeholder for your Vonage Application private key – you'll replace this with the downloaded file from VonageConfigure Your
.gitignore: Add common Node.js ignores and ensure.envand the private key are excluded.Define Your Environment Variables (
.env.example): List the variables you need. Copy this content into both.envand.env.example. Fill.envwith actual values later.Why Use
.env? Store credentials and configuration separate from code – crucial for security and flexibility across environments (development, staging, production).2. Configure Vonage for SMS and Webhooks
Set up your Vonage account and application to enable SMS sending and receiving with callbacks.
Reference: Vonage Applications Documentation
Log in to Your Vonage Dashboard: Access your account at dashboard.nexmo.com.
Set Your Default SMS API to "Messages API":
Create Your Vonage Application: Vonage Applications contain your communication configurations, including keys and webhooks.
private.keyfile. Save this in your project root, replacing the placeholderprivate.keyfile. Vonage stores the public key. These keys enable JWT authentication for Messages API requests.ngrokcomes in)Start ngrok: Open a new terminal window in your project directory and start
ngrok, forwarding traffic to the port your Express app will use (defaulting to 3000 from.env.example).ngrokprovidesForwardingURLs (http and https). Copy thehttpsURL – looks likehttps://random-subdomain.ngrok.io.Configure Your Webhook URLs:
ngrokhttps URL into the webhook fields, appending specific paths:YOUR_NGROK_HTTPS_URL/webhooks/inboundYOUR_NGROK_HTTPS_URL/webhooks/statusCopy Your Application ID: After creation, you'll see the application details page. Copy the Application ID – you need this for your
.envfile.Link Your Vonage Number:
Update Your
.envFile: Open your.envfile (the one without.example) and fill in the values you obtained:Security:
.envcontains secrets. Keep it in your.gitignoreand never commit to version control. Use environment variable management from your deployment platform in production.3. Write Your Core SMS Functionality
Build the Node.js/Express code to send and receive SMS.
File:
src/app.js// src/app.js // 1. Import Dependencies require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); // v3.x+ syntax const path = require('path'); // Needed for constructing the private key path // 2. Initialize Express App const app = express(); app.use(express.json()); // Middleware to parse JSON request bodies app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded request bodies // 3. Initialize Vonage Client // Ensure required environment variables are present if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) { console.error('ERROR: Set VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in your .env file'); process.exit(1); } if (!process.env.VONAGE_NUMBER) { console.error('ERROR: Set VONAGE_NUMBER in your .env file'); process.exit(1); } const privateKeyPath = path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH); let vonage; try { vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyPath // Provide the absolute path to the key file }); console.log("Vonage client initialized successfully."); } catch (error) { console.error("Error initializing Vonage client:", error); process.exit(1); } // 4. Define Route for Sending SMS (Example) // In production, **protect this endpoint** (e.g., API key check, user session authentication, rate limiting). Don't expose it publicly. app.post('/send-sms', async (req, res) => { const { to, text } = req.body; // Basic input validation if (!to || !text) { return res.status(400).json({ error: 'Missing required fields', message: 'Include both "to" (recipient number) and "text" (message content) in your request body' }); } if (!process.env.VONAGE_NUMBER) { return res.status(500).json({ error: 'Configuration error', message: 'Your Vonage number is missing from the configuration. Add VONAGE_NUMBER to your .env file.' }); } console.log(`Attempting to send SMS from ${process.env.VONAGE_NUMBER} to ${to}`); try { const resp = await vonage.messages.send({ channel: 'sms', message_type: 'text', to: to, // E.164 format (e.g., 14155550101) from: process.env.VONAGE_NUMBER, // Your Vonage number text: text }); console.log('Message sent successfully:', resp); // resp contains { message_uuid: '...' } on success res.status(200).json({ message_uuid: resp.message_uuid, status: 'Message submitted' }); } catch (err) { console.error('Error sending SMS:', err); // Provide more context if available const statusCode = err.response?.status || 500; const errorMessage = err.response?.data?.title || err.message || 'Failed to send SMS'; const errorDetails = { error: errorMessage, details: err.response?.data, help: statusCode === 401 ? 'Check your VONAGE_APPLICATION_ID and private.key file' : statusCode === 422 ? 'Verify your "to" number is in E.164 format (e.g., 14155550101)' : statusCode === 429 ? 'You have exceeded the rate limit. Wait before retrying.' : 'Check the details field for more information' }; res.status(statusCode).json(errorDetails); } }); // 5. Define Webhook Endpoint for Inbound SMS app.post('/webhooks/inbound', (req, res) => { console.log('--- Inbound SMS Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // TODO: Process the inbound message (req.body) // Example: Store in DB, trigger a response, etc. // Key fields: req.body.from, req.body.to, req.body.text, req.body.message_uuid, req.body.timestamp // Acknowledge receipt to Vonage immediately res.status(200).end(); // Why 200? Vonage expects a 200 OK response. Without one // within the timeout period, Vonage assumes failure and retries the webhook, // potentially causing duplicate processing. }); // 6. Define Webhook Endpoint for Delivery Status Updates app.post('/webhooks/status', (req, res) => { console.log('--- Delivery Status Update Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // TODO: Process the status update (req.body) // Example: Update message status in DB using req.body.message_uuid // Key fields: req.body.message_uuid, req.body.status ('delivered', 'failed', 'rejected', 'accepted', 'buffered'), // req.body.timestamp, req.body.error (if failed/rejected) // Acknowledge receipt to Vonage res.status(200).end(); }); // 7. Basic Health Check Endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); }); // 8. Define Port and Start Server (Only if run directly) const port = process.env.APP_PORT || 3000; if (require.main === module) { // Check if the script is being run directly app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); // Remind user about ngrok if not in production environment if (process.env.NODE_ENV !== 'production') { console.log(`Ensure ngrok is running and forwarding to this port.`); console.log(`Configure these webhook URLs in your Vonage Application:`); console.log(` Inbound: YOUR_NGROK_HTTPS_URL/webhooks/inbound`); console.log(` Status: YOUR_NGROK_HTTPS_URL/webhooks/status`); } }); } // 9. Basic Error Handling Middleware (Optional but Recommended) app.use((err, req, res, next) => { console.error('Unhandled Error:', err.stack || err); res.status(500).json({ error: 'Internal server error', message: 'Something went wrong. Check the server logs for details.', details: process.env.NODE_ENV === 'development' ? err.message : undefined }); }); // 10. Export the app for testing module.exports = app;Code Breakdown:
dotenv,express,Vonage SDK,path).dotenv.config()loads variables from.envJSONandURL-encodedrequest bodies – crucial for handling webhook payloadsapplicationIdandprivateKeypath from environment variables. Includes error checking for missing variables. Usespath.resolveto ensure the path toprivate.keyis correct regardless of where you run the script/send-smsRoute: APOSTendpoint to send SMS. Expectsto(recipient number) andtext(message content) in the JSON body. Validates input and callsvonage.messages.send()using thesmschannel. Returns success/error responses with helpful messages. Includes a warning about production protection/webhooks/inboundRoute: APOSTendpoint matching your Inbound URL in the Vonage Application. Vonage sends data here when your virtual number receives SMS. Logs the payload (req.body), includes aTODOfor your logic, and immediately sends200 OK. Send the 200 OK quickly before heavy processing./webhooks/statusRoute: APOSTendpoint matching your Status URL. Vonage sends delivery status updates here for messages you sent. Logs the payload, includes aTODO, and sends200 OK/healthRoute: Simple endpoint for monitoring systems to check if your application is runningAPP_PORT(defaulting to 3000) and starts the Express server only if you run the script directly (usingif (require.main === module)). Includes helpful reminders aboutngrokconfiguration during developmentappinstance for testing frameworks like Supertest4. Run and Test Locally
Ensure ngrok is running (from Step 2.4) and forwarding to port 3000. Verify the HTTPS URL matches the one in your Vonage Application webhook settings.
Verify your
.envfile contains your Vonage Application ID, number, and the correct path toprivate.key.Start Your Node.js Application: In the terminal where you created the project (not the
ngrokone), run:You should see "Server listening at http://localhost:3000" and the
ngrokreminder messages.Test Sending SMS: Use
curlor Postman to send a POST request to your local/send-smsendpoint. ReplaceYOUR_PHONE_NUMBERwith your mobile number in E.164 format (e.g.,14155550101).curl -X POST http://localhost:3000/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "YOUR_PHONE_NUMBER", "text": "Hello from Node.js Vonage App!" }'node src/app.jslogs "Attempting to send SMS…" then "Message sent successfully: …" with amessage_uuidcurlcommand receives JSON:{"message_uuid":"...","status":"Message submitted"}Test Receiving Status Updates:
node src/app.jslogs "--- Delivery Status Update Received ---" followed by the JSON payload from Vonage. Look for themessage_uuidmatching your sent message and astatusfield (e.g.,delivered,accepted,buffered) – exact status depends on carrier supportTest Receiving Inbound SMS:
node src/app.jslogs "--- Inbound SMS Received ---" followed by the JSON payload containing message details (from,text, etc.)5. Implement Error Handling, Logging, and Retries
Build production-ready error handling and logging.
Best Practices (October 2025):
Error Handling:
try...catchblock aroundvonage.messages.send()handles specific API call errors. Inspect theerrobject (especiallyerr.response.dataorerr.message) for details from the Vonage API401(authentication failed) – Check your VONAGE_APPLICATION_ID and private.key file403(forbidden) – Verify permissions and account status422(validation error) – Ensure phone numbers are in E.164 format429(rate limit exceeded) – Implement exponential backoff retries500(server error) – Retry with exponential backoff/webhooks/inbound,/webhooks/status) intry...catchblocks. Log errors but still send a200 OKresponse to Vonage to prevent retries (unless the request is fundamentally invalid)app.use((err, req, res, next) => {...})) catches errors not caught in specific routesLogging:
pino(recommended for performance) orwinstonin production instead ofconsole.logmessage_uuid), configuration issuesRetries:
200 OKwithin ~30 seconds. Respond quickly with200 OK5xxerror from Vonage), implement retry strategy with exponential backoff. Use libraries likeasync-retry200 OK(e.g., database write fails), log the error and use a background job queue (BullMQ, Kue, RabbitMQ) to retry the processing task independently6. Add Database Integration (Optional but Recommended)
Integrate a database for persistence and tracking.
Recommended ORMs/ODMs (October 2025):
Design Your Schema: Create a
messagestable:message_uuid(VARCHAR/TEXT, PRIMARY KEY/UNIQUE) - From Vonage response/webhooksdirection(ENUM/VARCHAR: 'outbound', 'inbound')to_number(VARCHAR)from_number(VARCHAR)body(TEXT)status(VARCHAR, e.g., 'submitted', 'delivered', 'failed', 'read') - Updated via status webhookvonage_timestamp(TIMESTAMP WITH TIME ZONE) - From webhook payloaderror_code(VARCHAR, nullable) - From status webhook if failed/rejectedcreated_at(TIMESTAMP WITH TIME ZONE)updated_at(TIMESTAMP WITH TIME ZONE)Implement Your Database Logic:
vonage.messages.send(), insert a record withmessage_uuid,direction='outbound',to,from,text,status='submitted', and timestampsreq.body.message_uuidand update itsstatus,vonage_timestamp,error_code(if present), andupdated_at. Handle cases where themessage_uuidmight not be found (log an error)direction='inbound', details fromreq.body,status='received', and timestamps7. Secure Your Application
Protect your application and user data.
Secure Your Webhooks:
httpsURLs for webhooks (required by Vonage as of October 2025)/webhooks/inboundand/webhooks/statusare reasonably specificManage Your Secrets:
.envlocally, platform-provided secrets in production). Don't commit.envorprivate.keyValidate Your Input:
/send-sms). Ensure required fields are present, types are correct, and formats match (e.g., E.164 for phone numbers). Use libraries likeexpress-validatororjoidompurify(if rendering in HTML)Implement Rate Limiting:
/send-smsif exposed) from abuse. Use middleware likeexpress-rate-limitUse HTTPS:
ngrokprovides this locally. Use a load balancer or reverse proxy (Nginx, Caddy, or platform services like AWS ALB/Cloudflare) to handle SSL termination in production8. Handle Special SMS Cases
Address real-world SMS nuances.
Format Your Numbers:
+14155550101or14155550101without spaces) when sendingtonumbers to Vonage. Vonage providesfromnumbers in webhooks in similar formatUnderstand Character Encoding & Concatenation:
Handle Opt-Outs (STOP):
Interpret Delivery Statuses:
deliveredstatus updates (DLRs – Delivery Receipt Reports)submitted,accepted,buffered,delivered,failed,rejected,expiredfailedorrejectedstatuses reliably indicate non-deliveryManage Time Zones:
timestamp) are in UTC (ISO 8601 format:YYYY-MM-DDTHH:MM:SS.sssZ)9. Optimize Your Performance
Ensure your webhook handlers are fast and efficient.
Respond Quickly:
200 OKresponse within 30 seconds (Vonage timeout). Aim for <1 second response time – this is your most critical optimizationProcess in the Background:
Index Your Database:
message_uuid(PRIMARY KEY or UNIQUE index)status(if you query by status frequently)created_at(for time-range queries)Cache Frequently Accessed Data:
10. Monitor Your Application
Understand how your application performs and diagnose issues.
Implement Health Checks:
/healthendpoint is your starting point. Production monitoring systems (AWS CloudWatch, Datadog, Prometheus/Grafana, UptimeRobot) can ping this endpoint to verify your app is liveTrack Your Metrics:
sendcalls)prom-clientfor Prometheus or integrate with APM tools (Datadog APM, New Relic)Track Your Errors:
Aggregate Your Logs:
Create Your Dashboards:
11. Troubleshoot Common Issues
Solve common problems and their solutions.
Webhooks Not Reaching Your App:
ngrokrunning? Free URLs expire after 2-8 hours – restart ngrok and update Vonage Application webhook URLsYour App Receives Webhooks but Errors Occur:
req.bodycorrectly? Ensureexpress.json()middleware is usedres.status(200).end()reliably and quickly? Check the Vonage Dashboard (Logs section) for webhook retry attemptsSMS Sending Fails:
VONAGE_APPLICATION_IDand the path/content ofVONAGE_PRIVATE_KEY_PATHin.envVONAGE_NUMBERcorrectly set in.envand linked to the Vonage Application used for initialization?errobject from thecatchblock in/send-sms–err.response.dataoften contains detailed error codes and messages from VonageNo Delivery Status Updates (
/webhooks/statusnot hit):Messages API vs. Legacy SMS API:
vonage.messages.send()(Messages API) vs.vonage.sms.send()(legacy SMS API)12. Deploy Your Application
Move from local development to production.
Choose Your Deployment Platform (October 2025):
Serverless/PaaS Options:
Container/VM Options:
Handle Your Private Key Securely: Provide the
private.keyfile to your production environment securely:VONAGE_PRIVATE_KEY_BASE64, decode at runtimeRun Your Build Process: If using TypeScript or a build step, ensure it runs before deployment
Manage Your Process: Use a process manager like
pm2or rely on the platform's built-in service management (e.g., systemd, Heroku dynos) to keep your Node.js app running, handle restarts, and manage logsConfigure HTTPS: Ensure your deployment platform handles HTTPS termination (most PaaS/Serverless platforms do) or configure a reverse proxy (Nginx, Caddy) if deploying to a VM
Set Up Your CI/CD Pipeline: Modern CI/CD tools: