code examples
code examples
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.
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:
- 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
.envfile intoprocess.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 --> DBWhat 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.
bashmkdir vonage-sms-app cd vonage-sms-app -
Initialize Your Node.js Project: Initialize with npm (or yarn). The
-yflag accepts default settings.bashnpm init -yThis 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.bashnpm install express @vonage/server-sdk dotenvCurrent Versions (October 2025):
express: v4.18.x or higher@vonage/server-sdk: v3.15.x or higherdotenv: v16.x or higher
-
Create Your Project Structure: Build a basic structure for clarity.
bashmkdir 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 Vonage
-
Configure Your
.gitignore: Add common Node.js ignores and ensure.envand the private key are excluded.text# .gitignore # Dependencies node_modules/ # Environment Variables .env* !.env.example # Vonage Private Key private.key # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db -
Define Your Environment Variables (
.env.example): List the variables you need. Copy this content into both.envand.env.example. Fill.envwith actual values later.dotenv# .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.ioWhy 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":
- 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.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. - 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
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).bashngrok http 3000ngrokprovidesForwardingURLs (http and https). Copy thehttpsURL – looks likehttps://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
- Inbound URL:
- 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
.envfile. -
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
.envFile: Open your.envfile (the one without.example) and fill in the values you obtained:dotenv# .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=3000Security:
.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:
- Dependencies: Imports necessary modules (
dotenv,express,Vonage SDK,path).dotenv.config()loads variables from.env - Express Init: Creates the Express app and adds middleware to parse incoming
JSONandURL-encodedrequest bodies – crucial for handling webhook payloads - Vonage Init: Initializes the Vonage client using
applicationIdandprivateKeypath 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 running- Server Start: Reads the port from
APP_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 development - Error Handler: Basic Express error-handling middleware to catch unhandled errors in route handlers
- Export: Exports the
appinstance 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
.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:bashnode src/app.jsYou 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).bashcurl -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.jslogs "Attempting to send SMS…" then "Message sent successfully: …" with amessage_uuid - The
curlcommand receives JSON:{"message_uuid":"...","status":"Message submitted"} - You receive the SMS on your phone shortly
- Your terminal running
- Expected Result:
-
Test Receiving Status Updates:
- Wait a few moments after sending the SMS
- Expected Result:
- Your terminal running
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 support
- Your terminal running
-
Test Receiving Inbound SMS:
- From your mobile phone, send an SMS to your Vonage virtual number
- Expected Result:
- Your terminal running
node src/app.jslogs "--- Inbound SMS Received ---" followed by the JSON payload containing message details (from,text, etc.)
- Your terminal running
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...catchblock aroundvonage.messages.send()handles specific API call errors. Inspect theerrobject (especiallyerr.response.dataorerr.message) for details from the Vonage API - Common Error Codes:
401(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
- Webhook Errors: Wrap logic inside your webhook handlers (
/webhooks/inbound,/webhooks/status) intry...catchblocks. Log errors but still send a200 OKresponse 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
- Vonage SDK Errors: The
-
Logging:
- Use
pino(recommended for performance) orwinstonin production instead ofconsole.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
- Use
-
Retries:
- Vonage Webhook Retries: Vonage automatically retries webhooks if they don't receive
200 OKwithin ~30 seconds. Respond quickly with200 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,
5xxerror from Vonage), implement retry strategy with exponential backoff. Use libraries likeasync-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
- Vonage Webhook Retries: Vonage automatically retries webhooks if they don't receive
6. Add Database Integration (Optional but Recommended)
Integrate a database for persistence and tracking.
Recommended ORMs/ODMs (October 2025):
- Prisma (SQL databases – PostgreSQL, MySQL, SQLite): Modern, type-safe ORM with excellent Developer Experience
- Drizzle ORM (SQL databases): Lightweight, TypeScript-first ORM
- Sequelize (SQL databases): Mature, feature-rich ORM
- 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/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:
- Sending: After successfully calling
vonage.messages.send(), insert a record withmessage_uuid,direction='outbound',to,from,text,status='submitted', and timestamps - Status Webhook: Find the message by
req.body.message_uuidand update itsstatus,vonage_timestamp,error_code(if present), andupdated_at. Handle cases where themessage_uuidmight not be found (log an error) - Inbound Webhook: Insert a new record with
direction='inbound', details fromreq.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
httpsURLs for webhooks (required by Vonage as of October 2025) - Endpoint Secrecy: Avoid overly simple webhook paths. The paths
/webhooks/inboundand/webhooks/statusare 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
- HTTPS: Essential – encrypts data in transit. Always use
Manage Your Secrets:
- Use environment variables (
.envlocally, platform-provided secrets in production). Don't commit.envorprivate.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 likeexpress-validatororjoi - 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-smsif exposed) from abuse. Use middleware likeexpress-rate-limit
Use HTTPS:
- Always run your production application behind 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 production
8. Handle Special SMS Cases
Address real-world SMS nuances.
Format Your Numbers:
- Always use E.164 format (e.g.,
+14155550101or14155550101without spaces) when sendingtonumbers to Vonage. Vonage providesfromnumbers in webhooks in similar format - Reference: E.164 Format
Understand Character Encoding & Concatenation:
- Standard SMS (GSM-7): 160 characters per segment
- Unicode (UCS-2): 70 characters per segment (required for emojis, non-Latin scripts)
- Concatenated messages: 153 chars (GSM-7) or 67 chars (UCS-2) per segment when splitting longer messages
- Vonage handles sending long messages automatically – you pay per segment
- Reference: SMS Concatenation and Encoding
Handle Opt-Outs (STOP):
- 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
deliveredstatus updates (DLRs – Delivery Receipt Reports) - Common statuses:
submitted,accepted,buffered,delivered,failed,rejected,expired - Design your logic to handle missing or delayed DLRs
failedorrejectedstatuses 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 OKresponse 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
/healthendpoint 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
sendcalls) - Error rates (webhook failures, API call failures)
- Queue sizes (if using background jobs)
- Use libraries like
prom-clientfor 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
ngrokrunning? 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.bodycorrectly? Ensureexpress.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_IDand the path/content ofVONAGE_PRIVATE_KEY_PATHin.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_NUMBERcorrectly set in.envand linked to the Vonage Application used for initialization? - API Errors: Log the
errobject from thecatchblock in/send-sms–err.response.dataoften 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)
- Railway.app: Modern alternative to Heroku
- Fly.io: Global edge deployment
- DigitalOcean App Platform: Simple containerized deployments
-
Container/VM Options:
- AWS ECS/EKS: Production-grade container orchestration
- Google Kubernetes Engine (GKE): Kubernetes-managed
- DigitalOcean Droplets: Traditional VMs
- Linode/Vultr: Cost-effective VPS options
-
Handle Your Private Key Securely: Provide the
private.keyfile to your production environment securely:- Option 1 (Recommended): Store the key content as base64 in environment variable
VONAGE_PRIVATE_KEY_BASE64, decode at runtime - Option 2: Use secrets management: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault
- Option 3: For serverless: Include private.key in deployment bundle (ensure it's in .gitignore)
- Never commit private.key to version control
- Option 1 (Recommended): Store the key content as base64 in environment variable
-
Run 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 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