code examples
code examples
Developer Guide: Implementing Two-Way SMS Messaging with Node.js, Express, and Vonage
Complete tutorial for building production-ready two-way SMS messaging with Node.js, Express, and Vonage Messages API. Learn webhook handling, JWT authentication, inbound/outbound SMS, E.164 format, and auto-reply implementation with code examples.
Last Updated: October 5, 2025
Developer guide: Implementing two-way SMS messaging with Node.js, Express, and Vonage
This guide provides a complete walkthrough for building a production-ready Node.js application using the Express framework to handle two-way SMS messaging via the Vonage Messages API. You'll learn how to send outbound SMS messages and set up webhooks to receive inbound messages and delivery statuses, enabling interactive communication flows.
Note: Vonage (formerly Nexmo) provides the Messages API, which supersedes the legacy SMS API with enhanced features and unified multi-channel support.
Source: Vonage Developer Documentation (developer.vonage.com, verified October 2025)
By the end of this guide, you will have a functional Node.js application capable of:
- Sending SMS messages programmatically using the Vonage Messages API.
- Receiving incoming SMS messages via a webhook endpoint.
- Receiving message delivery status updates via a webhook endpoint.
- Handling basic two-way interaction (e.g., auto-replying to incoming messages).
We assume you have a basic understanding of Node.js, asynchronous programming (Promises, async/await), and REST APIs.
Project overview and goals
Goal: To create a robust Node.js service that can both send and receive SMS messages using Vonage's communication capabilities.
Problem Solved: This implementation enables applications to engage users via SMS for notifications, alerts, two-factor authentication (2FA), customer support, marketing campaigns, or any scenario requiring direct mobile communication. It provides the foundation for building interactive SMS-based services.
<!-- EXPAND: Could benefit from real-world use case examples with cost estimates (Type: Enhancement) -->Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications, especially I/O-intensive ones like webhook handlers.
- Express: A minimal and flexible Node.js web application framework that simplifies creating API endpoints and handling HTTP requests, perfect for our webhook server.
- Vonage Messages API: A unified API from Vonage enabling communication across multiple channels (SMS, MMS, WhatsApp, Viber, Facebook Messenger). We focus on SMS for this guide. It provides reliable message delivery and robust webhook features.
@vonage/server-sdk: The official Vonage Node.js SDK simplifies interaction with Vonage APIs.ngrok: A tool to expose local development servers to the internet, essential for testing Vonage webhooks without deploying. Note: Free tier has session time limits (typically 2 hours) and URLs change on restart.dotenv: A module to load environment variables from a.envfile intoprocess.env, keeping sensitive credentials out of source code.
Source: npm package registry (@vonage/server-sdk, verified October 2025), ngrok documentation
System Architecture:
+-------------+ +-----------------+ +---------------------+ +---------------------+
| User's Phone| <---->| Vonage Platform | <---->| Your Node.js/Express| ----> | (Optional) Database |
| (SMS) | | (Messages API) | | App (Webhook Server)| | / Other Services |
+-------------+ +-----------------+ +---------------------+ +---------------------+
| ^ | ^
| Send SMS | | Receive SMS | Store/Process Data
|______________________| |______________________|
| | |
| Delivery Status | | Outbound SMS Request
v | v
+---------------------+ | +---------------------+
| Your Node.js/Express| <---- | Vonage Platform |
| App (Sending Logic) | | (Messages API) |
+---------------------+ +---------------------+Expected Outcome: A running Node.js application with two main parts:
- A script (
send-sms.js) to send an outbound SMS on demand. - An Express server (
server.js) listening for incoming HTTP POST requests from Vonage webhooks (inbound messages and status updates).
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. (Node.js v18+ LTS recommended as of October 2025).
- Vonage API Account: Sign up if you don't have one. You'll get free credits to start.
- Vonage API Key and Secret: Found on your Vonage API Dashboard.
- Vonage Virtual Phone Number: Purchase an SMS-capable number from the Vonage dashboard (e.g., via Numbers -> Buy Numbers). Note: Number must support SMS capability and be in E.164 format (+country_code + number).
ngrok: Installed and authenticated (a free account is sufficient). Download from ngrok.com. Development only – not for production use.- Vonage CLI (Optional but Recommended): Install via
npm install -g @vonage/cli. Useful for managing applications and numbers.
Source: ITU-T Recommendation E.164 (phone number format standard)
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir vonage-sms-app cd vonage-sms-app -
Initialize Node.js Project: This creates a
package.jsonfile to manage project details and dependencies.bashnpm init -y -
Install Dependencies: Install
expressfor the web server,@vonage/server-sdkto interact with the Vonage API, anddotenvfor managing environment variables.bashnpm install express @vonage/server-sdk dotenv -
Project Structure: Create the following basic structure:
plaintextvonage-sms-app/ ├── node_modules/ ├── .env # Stores environment variables (DO NOT COMMIT) ├── .gitignore # Specifies intentionally untracked files ├── private.key # Vonage application private key (DO NOT COMMIT) ├── package.json ├── package-lock.json ├── send-sms.js # Script to send outbound SMS └── server.js # Express server for receiving webhooks -
Configure
.gitignore: Create a.gitignorefile in the root directory to prevent committing sensitive information and unnecessary files.plaintext# .gitignore node_modules .env private.key *.log .DS_Store
Why .gitignore? This ensures your API secrets, private keys, and local environment configurations are not accidentally pushed to version control systems like Git, which is crucial for security.
2. Vonage configuration
<!-- DEPTH: Configuration section needs screenshots or visual aids for dashboard navigation (Priority: High) -->Before writing code, configure your Vonage account and application settings. Note: Specific UI labels and navigation paths within the Vonage Dashboard may change over time. These instructions were verified October 2025.
-
API Key and Secret: Navigate to your Vonage API Dashboard. Your API Key and API Secret are displayed prominently at the top. You'll need these shortly.
-
Purchase a Vonage Number:
- Go to
Numbers->Buy Numbers(or equivalent section) in the dashboard. - Search for a number with SMS capability in your desired country.
- Purchase the number. Note down this number. It will be your
VONAGE_NUMBER.
- Go to
-
Select Messages API for SMS:
- Crucially, Vonage has both an older SMS API and the newer Messages API. They use different webhook formats, authentication methods, and payload structures. This guide uses the Messages API.
- Go to your API Settings page.
- Under
SMS Settings, ensureDefault SMS Settingis set toUse the Messages API. - Click
Save changes.
Why this setting? It ensures that webhooks related to your Vonage number use the Messages API format and the
@vonage/server-sdkmethods we'll use. The legacy SMS API uses different webhook schemas.Source: Vonage API Settings documentation (developer.vonage.com, verified October 2025)
-
Create a Vonage Application: The Messages API uses Applications for authentication via JWT (JSON Web Token) with public/private key pairs, which is more secure than using only API Key/Secret. The Application also defines webhook endpoints.
- Go to
Applications->Create a new application(or equivalent). - Give your application a name (e.g.,
Nodejs SMS App). - Click
Generate public and private key. Immediately save theprivate.keyfile that downloads. Move this file into your project's root directory (where.gitignorewill prevent committing it). - Enable the
Messagescapability. - You'll need to provide Webhook URLs:
- Inbound URL:
YOUR_NGROK_URL/webhooks/inbound(We'll getYOUR_NGROK_URLlater using ngrok). - Status URL:
YOUR_NGROK_URL/webhooks/status
- Inbound URL:
- Leave these blank for now or enter temporary placeholders like
http://example.com/inbound. We will update them after startingngrok. - Click
Generate new application. - Note down the Application ID provided.
Source: Vonage Applications API documentation (developer.vonage.com/en/application/overview)
- Go to
-
Link Your Number to the Application:
- Go to
Numbers->Your numbers. - Find the Vonage number you purchased.
- Click the
Edit(pencil) icon orManagebutton next to the number. - In the
ForwardingorApplicationsection, select the Vonage Application you just created (Nodejs SMS App) from the dropdown underMessages. - Save the changes.
Why link the number? This tells Vonage to route incoming messages and status updates for this specific number to the webhooks defined in the linked Application.
- Go to
3. Environment setup (.env)
Create the .env file in your project root and add your credentials. Replace the placeholder values with your actual details.
# .env
# Vonage API Credentials (Found on Dashboard)
# Used by the SDK for some operations or legacy API access.
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application Details (From Application creation)
# Primary authentication method for the Messages API client.
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
# Path relative to project root
VONAGE_PRIVATE_KEY_PATH=./private.key
# Vonage Number (Purchased Number) – Use E.164 format (e.g., +12015550123)
VONAGE_NUMBER=YOUR_VONAGE_NUMBER
# Test Recipient Number (Your mobile number) – Use E.164 format
MY_TEST_NUMBER=YOUR_MOBILE_NUMBERVONAGE_API_KEY,VONAGE_API_SECRET: Used by the SDK for certain operations (e.g., some account management functions) or potentially as fallback authentication if Application ID/Key isn't configured for a specific client.VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATH: The primary credentials used by the Messages API client in this guide for sending messages via JWT generation using your private key.VONAGE_NUMBER: The Vonage virtual number you purchased and linked to the application. This will be the sender (from) number. Use E.164 format (+country_code + number, no spaces/symbols, e.g., +14155552671).MY_TEST_NUMBER: The destination number (to) for sending test messages. Use your own mobile number in E.164 format (country code + number, no spaces or symbols, e.g.,+14155552671).
Source: ITU-T Recommendation E.164 standard for international phone numbering
4. Sending SMS messages
<!-- DEPTH: Code example lacks inline comments explaining SMS delivery flow and timing expectations (Priority: Medium) -->Create the script (send-sms.js) to send an outbound SMS.
// send-sms.js
'use strict';
// Load environment variables from .env file
require('dotenv').config();
// Import the Vonage Server SDK
const { Vonage } = require('@vonage/server-sdk');
const { MessengerText } = require('@vonage/messages');
// Initialize Vonage client with application credentials
// This uses Application ID and Private Key for Messages API authentication
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// Function to send an SMS message
async function sendSms(to, text) {
const from = process.env.VONAGE_NUMBER;
if (!from || !to || !text) {
console.error('Missing required environment variables or parameters.');
console.error(`FROM: ${from}, TO: ${to}, TEXT: ${text ? 'Provided' : 'Missing'}`);
return; // Exit if essential info is missing
}
console.log(`Attempting to send SMS from ${from} to ${to}…`);
try {
const resp = await vonage.messages.send(
new MessengerText({
text: text,
to: to,
from: from,
channel: 'sms', // Specify the channel as SMS
})
);
console.log('Message sent successfully!');
console.log('Message UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending SMS:');
// Log the detailed error response if available
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
}
}
// --- Script Execution ---
// Get recipient number and message text
// For simplicity, using environment variable for recipient
// In a real app, 'to' and 'text' might come from user input, DB, etc.
const recipientNumber = process.env.MY_TEST_NUMBER;
const messageText = 'Hello from Vonage and Node.js!';
// Call the function to send the SMS
sendSms(recipientNumber, messageText);Explanation:
require('dotenv').config();: Loads variables from your.envfile intoprocess.env.@vonage/server-sdk: Imports the necessary Vonage SDK components. We useVonagefor the client andMessengerTextas a helper class for constructing the SMS payload correctly for the Messages API.new Vonage(...): Initializes the client. Crucially, for the Messages API, we provideapplicationIdandprivateKey. The SDK handles JWT generation for authentication behind the scenes.sendSmsfunction:- Takes the recipient number (
to) and message content (text) as arguments. - Retrieves the
fromnumber (your Vonage number) from environment variables. - Includes basic validation to ensure required parameters are present.
- Uses
vonage.messages.send()which is the method for the Messages API. - Constructs the payload using
new MessengerText({...}), specifyingchannel: 'sms'. - Uses
async/awaitfor cleaner asynchronous code. - Logs the
messageUuidon success, which is useful for tracking. - Includes
try...catchfor robust error handling, logging the detailed error response from Vonage if available.
- Takes the recipient number (
- Script Execution: Sets the recipient and text, then calls
sendSms.
Run the Sending Script:
Execute the script from your terminal:
node send-sms.jsYou should see output indicating the attempt and success (with a message UUID) or failure (with error details). Check your mobile phone for the incoming SMS!
5. Receiving SMS messages (Webhook Server)
Now, build the Express server (server.js) to handle incoming webhooks from Vonage.
1. Start ngrok:
Vonage needs a publicly accessible URL to send webhooks to. ngrok creates a secure tunnel from the internet to your local machine.
Important limitations:
- Development only: ngrok (especially free tier) is suitable for development and testing only, not for production environments
- Session limits: Free tier sessions timeout after approximately 2 hours
- URL changes: URLs change each time you restart ngrok (paid plans offer static URLs)
- Security: Consider using ngrok authentication features or IP restrictions for added security
Source: ngrok documentation (ngrok.com/docs)
Open a new terminal window (keep the first one for running the Node server later) and run:
ngrok http 3000Why port 3000? This matches the port our Express server will listen on.
ngrok will display output similar to this:
ngrok (Ctrl+C to quit)
Session Status online
Account Your Name (Plan: Free)
Version x.x.x
Region United States (us-cal-1)
Latency xx.xxms
Web Interface http://127.0.0.1:4040
Forwarding https://<RANDOM_SUBDOMAIN>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00Copy the https://<RANDOM_SUBDOMAIN>.ngrok-free.app URL. This is YOUR_NGROK_URL.
2. Update Vonage Application Webhooks:
- Go back to your Vonage Application settings in the dashboard (
Applications-> Your Application Name). - Paste your
ngrokForwarding URL into the webhook fields:- Inbound URL:
https://<RANDOM_SUBDOMAIN>.ngrok-free.app/webhooks/inbound - Status URL:
https://<RANDOM_SUBDOMAIN>.ngrok-free.app/webhooks/status
- Inbound URL:
- Ensure the HTTP Method for both is set to POST.
- Click
Save changes.
3. Create the Express Server (server.js):
// server.js
'use strict';
// Load environment variables
require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000; // Use environment port or default to 3000
// --- Middleware ---
// Enable Express to parse JSON request bodies
app.use(express.json());
// Enable Express to parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// --- Webhook Endpoints ---
// Inbound SMS Webhook Endpoint
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound SMS Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { from, text, timestamp, message_uuid } = req.body;
// Basic validation
if (!from || !from.number || !text) { // Check from.number specifically
console.warn('Received incomplete inbound message data.');
// Still send 200 OK so Vonage doesn't retry
return res.status(200).send('Incomplete data received.');
}
console.log(`From: ${from.number}`); // Access sender number
console.log(`Text: ${text}`);
console.log(`Timestamp: ${timestamp}`);
console.log(`Message UUID: ${message_uuid}`);
// --- Add your logic here ---
// Example: Log to a file, store in a database, trigger another action,
// or implement an auto-reply (see section 6).
// --- IMPORTANT: Respond to Vonage ---
// Vonage expects a 200 OK response to acknowledge receipt of the webhook.
// Failure to respond (or responding with non-200) will cause Vonage to retry.
res.status(200).send('Webhook received successfully.');
// Use res.sendStatus(200); for an empty body response.
});
// Delivery Receipt (Status) Webhook Endpoint
app.post('/webhooks/status', (req, res) => {
console.log('--- Message Status Update Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { message_uuid, status, timestamp, to } = req.body;
// Basic validation
if (!message_uuid || !status || !timestamp || !to || !to.number) {
console.warn('Received incomplete status update data.');
return res.status(200).send('Incomplete status data received.');
}
console.log(`Message UUID: ${message_uuid}`);
console.log(`Status: ${status}`); // e.g., "delivered", "accepted", "failed", "rejected"
console.log(`Timestamp: ${timestamp}`);
console.log(`Recipient: ${to.number}`);
// --- Add your logic here ---
// Example: Update message status in your database based on message_uuid.
// Handle "failed" or "rejected" statuses appropriately (e.g., logging, alerts).
// --- IMPORTANT: Respond to Vonage ---
res.status(200).send('Status webhook received successfully.');
});
// --- Root Endpoint (Optional: for health checks/testing) ---
app.get('/', (req, res) => {
res.send('SMS Webhook Server is running!');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`ngrok should be forwarding to http://localhost:${PORT}`);
console.log('Waiting for Vonage webhooks…');
});
// Basic Error Handling Middleware (Optional but Recommended)
app.use((err, req, res, next) => {
console.error('An unexpected error occurred:', err.stack);
res.status(500).send('Something broke!');
});Explanation:
- Middleware (
express.json(),express.urlencoded()): Essential for parsing the incoming JSON and form-encoded data that Vonage sends in webhook requests. /webhooks/inboundEndpoint (POST):- This route matches the Inbound URL configured in your Vonage Application.
- It logs the entire request body (
req.body) received from Vonage. This is crucial for debugging and understanding the payload structure. - It extracts key fields like
from(an object containing the sender'snumber),text(the message content),timestamp, andmessage_uuid. Includes basic validation. - Crucially sends
res.status(200).send(...). This acknowledges receipt to Vonage. Without this, Vonage will assume failure and retry sending the webhook, leading to duplicate processing.
/webhooks/statusEndpoint (POST):- Matches the Status URL configured in the Vonage Application.
- Receives updates about the delivery status of outbound messages you sent.
- Logs the payload, extracting fields like
message_uuid,status("delivered", "failed", etc.),timestamp, andto(recipient). Includes basic validation. - Also sends a
200 OKresponse.
- Server Start (
app.listen): Starts the Express server on the specifiedPORT(defaulting to 3000 to matchngrok). - Basic Error Handling: A simple middleware catches unhandled errors.
4. Run the Webhook Server:
Go back to your first terminal window (where you ran npm install) and start the server:
node server.jsYou should see the "Server listening…" message.
5. Test Receiving:
- Send an SMS message from your mobile phone to your Vonage number.
- Watch the terminal where
node server.jsis running. You should see the "--- Inbound SMS Received ---" log, followed by the JSON payload of the incoming message. - Check the terminal where
ngrokis running. You should seePOST /webhooks/inbound 200 OKindicating the request was received and forwarded. - You can also inspect the request details in the
ngrokweb interface (usuallyhttp://127.0.0.1:4040).
6. Test Status Updates:
- Run the sending script again:
node send-sms.js. - Watch the
node server.jsterminal. After a short delay, you should see the "--- Message Status Update Received ---" log with the delivery status (likely "delivered" or "accepted" initially). - Check
ngroklogs/interface forPOST /webhooks/status 200 OK.
6. Putting it together: Basic two-way messaging (Auto-Reply)
Let's modify the server.js to automatically reply to incoming messages.
-
Refactor Sending Logic: Move the
sendSmsfunction fromsend-sms.jsintoserver.js(or a separate utility file) so the webhook handler can call it. Ensure@vonage/server-sdk,dotenv, and theVonageclient initialization are also present inserver.js. -
Modify
/webhooks/inboundHandler:
// server.js (Additions/Modifications)
'use strict';
require('dotenv').config();
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const { MessengerText } = require('@vonage/messages');
const app = express();
const PORT = process.env.PORT || 3000;
// Initialize Vonage client (as in send-sms.js)
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// --- Middleware ---
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// --- Reusable Send SMS Function ---
async function sendSms(to, text) {
const from = process.env.VONAGE_NUMBER;
if (!from || !to || !text) {
console.error('Auto-reply failed: Missing required parameters.');
console.error(`FROM: ${from}, TO: ${to}, TEXT: ${text ? 'Provided' : 'Missing'}`);
return;
}
console.log(`Attempting auto-reply from ${from} to ${to}…`);
try {
const resp = await vonage.messages.send(
new MessengerText({ text, to, from, channel: 'sms' })
);
console.log('Auto-reply sent successfully! UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending auto-reply:');
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
}
}
// --- Webhook Endpoints ---
app.post('/webhooks/inbound', async (req, res) => { // Note: async handler now
console.log('--- Inbound SMS Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// --- Acknowledge receipt IMMEDIATELY ---
// It's best practice to acknowledge the webhook quickly
// before doing potentially long-running tasks like sending another SMS.
res.status(200).send('Webhook received. Processing…');
// --- Process the message ---
const { from, text, timestamp, message_uuid } = req.body;
if (!from || !from.number || !text) {
console.warn('Received incomplete inbound message data. Cannot auto-reply.');
return; // Already sent 200 OK
}
console.log(`From: ${from.number}`);
console.log(`Text: ${text}`);
// --- Auto-Reply Logic ---
const replyText = `Thanks for your message: "${text}". We received it!`;
// Send the reply asynchronously AFTER acknowledging the webhook
await sendSms(from.number, replyText); // Use from.number as the recipient
});
// --- Status Webhook Endpoint (Keep as before) ---
app.post('/webhooks/status', (req, res) => {
console.log('--- Message Status Update Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { message_uuid, status, timestamp, to } = req.body;
if (!message_uuid || !status || !timestamp || !to || !to.number) {
console.warn('Received incomplete status update data.');
return res.status(200).send('Incomplete status data received.');
}
console.log(`Message UUID: ${message_uuid}`);
console.log(`Status: ${status}`);
console.log(`Timestamp: ${timestamp}`);
console.log(`Recipient: ${to.number}`);
// Add logic to handle status updates (e.g., update DB)
res.status(200).send('Status webhook received successfully.');
});
// --- Root Endpoint ---
app.get('/', (req, res) => {
res.send('SMS Webhook Server is running!');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
// --- Error Handling Middleware ---
app.use((err, req, res, next) => {
console.error('An unexpected error occurred:', err.stack);
res.status(500).send('Something broke!');
});Explanation of Changes:
- The
sendSmsfunction and Vonage client initialization are now part ofserver.js. - The
/webhooks/inboundhandler is markedasync. - Important: We now send
res.status(200).send(...)before processing the message and sending the reply. This prevents Vonage webhook timeouts if the reply takes time. - We extract the sender's number from
req.body.from.number. - We define
replyText. - We call
await sendSms(from.number, replyText)to send the reply back to the original sender.
Retest:
- Restart the server:
Ctrl+Cthennode server.js. - Send another SMS from your mobile to your Vonage number.
- Check the server logs – you should see the inbound message log.
- Check your mobile phone – you should receive the auto-reply message shortly after.
- Check the server logs again – you should see the status update for the auto-reply message.
7. Error handling and logging
<!-- DEPTH: Error handling section lacks specific error code examples and retry strategies (Priority: High) -->Our current setup uses basic console.log and console.error. For production:
- Use a Structured Logger: Implement a more robust logging library like Winston or Pino. This enables different log levels (debug, info, warn, error), formatting (like JSON), and sending logs to files or external services.
javascript
// Example with Pino (Conceptual) const pino = require('pino')(); // Replace console.log/error with pino.info, pino.error, etc. pino.info({ body: req.body }, 'Inbound SMS Received'); - Centralized Error Handling: The basic Express error middleware is a start. Expand it to format error responses consistently and potentially send alerts for critical errors.
- Webhook Retries: Remember Vonage retries webhooks on non-200 responses or timeouts. Ensure your handlers are idempotent (safe to run multiple times with the same input) or have mechanisms to detect duplicate
message_uuids if necessary, especially if writing to a database. Always respond200 OKquickly. - API Error Handling: The
try...catchblock aroundvonage.messages.sendis crucial. Parse theerr.response.datafor specific Vonage error codes and messages to understand failures (e.g., invalid number format, insufficient funds, carrier restrictions). Implement retry logic with exponential backoff for transient network errors when sending, if needed.
8. Security considerations
-
Webhook Security: Our current setup relies on the obscurity of the
ngrokURL. For production:- Signature Validation: Vonage Messages API webhooks include a JWT signature in the
Authorizationheader that you should verify to ensure requests genuinely originate from Vonage. The@vonage/server-sdkprovides methods for JWT verification.
javascript// Example: JWT verification (conceptual) const { Vonage } = require('@vonage/server-sdk'); const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH }); // In webhook handler app.post('/webhooks/inbound', (req, res) => { const token = req.headers['authorization']; try { // Verify the JWT token vonage.credentials.verifySignature(token); // Token is valid, process webhook } catch (error) { console.error('Invalid webhook signature'); return res.status(401).send('Unauthorized'); } // ... rest of handler });Source: Vonage webhook security documentation (developer.vonage.com/en/messages/concepts/signed-webhooks, verified October 2025)
- HTTPS Only: Always use HTTPS endpoints in production (ngrok provides HTTPS by default)
- IP Whitelisting: Configure your firewall/server to only accept requests from Vonage's documented webhook IP ranges (see Vonage documentation for current IPs)
- Signature Validation: Vonage Messages API webhooks include a JWT signature in the
-
Credential Management: Never commit
.envorprivate.key. Use environment variables provided by your hosting platform or secrets management tools (like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) in production. -
Input Validation/Sanitization: If you process the
textfrom incoming SMS messages (e.g., storing in DB, displaying in UI), sanitize it thoroughly to prevent Cross-Site Scripting (XSS), SQL injection, or other injection attacks. Libraries likeDOMPurify(for HTML contexts) or parameterized queries (for SQL) are essential. -
Rate Limiting: Protect your webhook endpoints from abuse or accidental loops by implementing rate limiting using Express middleware like
express-rate-limit.
Source: OWASP security guidelines, Vonage security best practices
9. Database schema and data layer (Conceptual)
<!-- DEPTH: Database section is too conceptual - needs practical ORM setup examples (Priority: Medium) -->This guide doesn't implement a database, but this section provides a conceptual overview. The schema and code snippets below are illustrative examples, not a complete, production-ready implementation.
- Schema: You'd likely need tables for:
messages: To store details of both inbound and outbound messages (UUID, direction, sender, recipient, text, timestamp, status, related message UUID for replies).conversations(Optional): To group messages belonging to the same interaction thread.
- Entity Relationship Diagram (ERD) – Conceptual:
plaintext
+--------------+ +--------------+ | conversations|--|--( messages | +--------------+ +--------------+ | conversation_id (PK)| message_id (PK)| | participant_a | | conversation_id (FK)| | participant_b | | direction | | last_update | | sender | +--------------+ | recipient | | text | | timestamp | | status | | vonage_uuid | +--------------+ - Data Access: Use an Object-Relational Mapping (ORM) tool like Prisma or Sequelize to interact with your database (PostgreSQL, MySQL, etc.).
- In
/webhooks/inbound: Create a newmessagesrecord with direction "inbound". - In
sendSms: Create a newmessagesrecord with direction "outbound" and status "submitted". - In
/webhooks/status: Find the message byvonage_uuid(message_uuid from webhook) and update itsstatus.
- In
- Migrations: Use the ORM's migration tools (e.g.,
prisma migrate dev) to manage schema changes.
10. Testing and verification
<!-- DEPTH: Testing section needs concrete test file examples with Jest setup (Priority: High) -->Testing is crucial for a reliable messaging application. The examples here are conceptual starting points.
- Manual Testing:
- Send SMS to your Vonage number -> Verify server logs inbound webhook -> Verify auto-reply is received on mobile.
- Run
send-sms.js(or trigger sending via your app logic) -> Verify SMS received on mobile -> Verify server logs status webhook. - Test edge cases: long messages (concatenated SMS), messages with special characters/emojis (Unicode encoding), invalid recipient numbers (E.164 format validation).
ngrokInspector: Usehttp://127.0.0.1:4040during development to inspect exact headers and payloads of incoming webhooks, which is invaluable for debugging.- Unit/Integration Tests: Use frameworks like Jest or Mocha.
- Unit test individual functions (like input validation, payload formatting, E.164 number validation).
- Integration test SDK interactions (
vonage.messages.send) by mocking the Vonage API client. - Integration test webhook handlers by sending mock HTTP requests with valid JWT signatures and asserting responses/side effects (like database writes or calls to
sendSms).
- End-to-End (E2E) Tests:
- These are more complex, often requiring a dedicated testing environment and potentially real Vonage numbers/credits.
- Simulate the full flow: send an SMS via the API -> verify receipt on a test device/simulator -> have the test device reply -> verify the inbound webhook is processed correctly -> verify the auto-reply is sent and received.
- Be mindful of SMS costs and potential rate limits when running E2E tests against live services.
- Consider using Vonage's sandbox environment if available for testing without incurring SMS charges.
Source: Testing best practices, Jest documentation, Vonage developer resources
<!-- GAP: Missing troubleshooting section for common setup and runtime issues (Type: Critical) -->Additional Resources
Official Documentation:
- Vonage Developer Portal – Complete API reference and guides
- Vonage Messages API Documentation – Detailed Messages API specification
- Vonage Node.js SDK – GitHub repository and examples
- E.164 Phone Number Format – ITU-T international numbering standard
Frequently Asked Questions
How do I set up two-way SMS messaging with Vonage and Node.js?
Set up two-way SMS messaging by: (1) Create a Vonage account and purchase an SMS-capable number, (2) Create a Vonage Application with Messages API capability and download the private key, (3) Install @vonage/server-sdk and express via npm, (4) Initialize the Vonage client with your Application ID and private key for JWT authentication, (5) Configure webhook endpoints for inbound messages and status updates using ngrok for local testing, (6) Implement Express POST routes at /webhooks/inbound and /webhooks/status to receive Vonage webhooks, (7) Link your Vonage number to the Application in the dashboard. The complete setup takes approximately 15–30 minutes and enables both sending outbound SMS via vonage.messages.send() and receiving inbound messages through webhook handlers.
What is the difference between Vonage Messages API and SMS API?
Vonage offers two APIs for SMS: the legacy SMS API and the newer Messages API. The Messages API (recommended for new projects) provides unified multi-channel support (SMS, MMS, WhatsApp, Viber, Facebook Messenger), uses JWT authentication with public/private key pairs for enhanced security, delivers richer webhook payloads with more detailed message metadata, and offers better delivery reporting. The SMS API (older) only supports SMS/MMS, uses basic API key/secret authentication, and has limited webhook functionality. Configure your Vonage account to use Messages API via API Settings -> Default SMS Setting -> Use the Messages API in the dashboard. This guide exclusively uses the Messages API for its superior security and features.
How do I authenticate Vonage Messages API requests with JWT?
Authenticate Messages API requests using JWT (JSON Web Token) by initializing the Vonage SDK with your Application ID and private key: new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: './private.key' }). The SDK automatically generates short-lived JWT tokens signed with your private key for each API request. Store your private.key file securely, add it to .gitignore to prevent committing it to version control, and use environment variables to reference the key path. For webhook security, verify incoming webhook signatures by extracting the JWT from the Authorization header and using vonage.credentials.verifySignature(token) to ensure requests genuinely originate from Vonage. JWT authentication is more secure than API key/secret because tokens expire quickly and the private key never leaves your server.
What phone number format does Vonage require for SMS?
Vonage requires E.164 international phone number format for all SMS operations. E.164 format consists of: + prefix, country code (1–3 digits), and subscriber number (up to 15 total digits, no spaces or special characters). Examples: +14155552671 (US), +442071838750 (UK), +61404123456 (Australia). Always include the country code even for domestic messages. Invalid formats (missing +, spaces, hyphens, parentheses) cause message delivery failures with error codes like "Invalid number format." Use validation regex /^\+[1-9]\d{1,14}$/ to verify E.164 compliance before sending. Store all phone numbers in E.164 format in your database to ensure consistency. The ITU-T E.164 standard defines this format for international telecommunication numbering.
How do I handle inbound SMS webhooks from Vonage?
Handle inbound SMS webhooks by creating an Express POST endpoint at /webhooks/inbound that: (1) Parses the JSON request body using express.json() middleware, (2) Extracts key fields (from.number, text, timestamp, message_uuid) from req.body, (3) Responds immediately with res.status(200).send() to acknowledge receipt (Vonage retries on non-200 responses or timeouts), (4) Processes the message asynchronously after acknowledging (log to database, trigger business logic, send auto-reply). Make the route handler async if performing asynchronous operations like database writes or sending replies. Verify webhook authenticity in production by validating the JWT signature in the Authorization header using vonage.credentials.verifySignature(). Use ngrok to expose your local development server for testing: ngrok http 3000 provides a public HTTPS URL to configure in your Vonage Application webhook settings.
What are the limitations of using ngrok for Vonage webhooks?
ngrok limitations for Vonage webhook development include: (1) Development only – ngrok free tier is unsuitable for production due to reliability and security concerns, (2) Session timeouts – free tier sessions expire after approximately 2 hours requiring restart and webhook URL reconfiguration, (3) Changing URLs – each ngrok restart generates a new random subdomain requiring Vonage Application webhook updates (paid plans offer static URLs), (4) No guaranteed uptime – ngrok sessions can disconnect unexpectedly, (5) Security concerns – free URLs are publicly accessible without authentication (use ngrok's auth features or IP restrictions for added security). For production, deploy your webhook server to a cloud platform (AWS, Google Cloud, Azure, Heroku, Vercel) with a static HTTPS URL and configure that permanent URL in your Vonage Application settings.
How do I send an auto-reply to inbound SMS messages?
Send auto-replies by: (1) Initialize the Vonage client in server.js with Application ID and private key, (2) Create a reusable sendSms(to, text) async function using vonage.messages.send(new MessengerText({ text, to, from: process.env.VONAGE_NUMBER, channel: 'sms' })), (3) In the /webhooks/inbound handler, respond 200 OK immediately to acknowledge the webhook, (4) Extract the sender's number from req.body.from.number, (5) Call await sendSms(from.number, replyText) to send the reply back to the original sender. Ensure the reply is sent after acknowledging the webhook to prevent Vonage timeouts. Test by sending an SMS to your Vonage number – you should receive the auto-reply within seconds and see both inbound and outbound status webhooks in your server logs.
How do I verify webhook signatures from Vonage?
Verify webhook signatures by implementing JWT validation: (1) Extract the JWT token from the Authorization header in webhook requests: const token = req.headers['authorization'], (2) Use the Vonage SDK verification method: vonage.credentials.verifySignature(token) wrapped in a try-catch block, (3) Respond 401 Unauthorized if verification fails: return res.status(401).send('Unauthorized'), (4) Process the webhook only after successful verification. Initialize the Vonage client with both API key/secret and Application ID/private key for signature verification: new Vonage({ apiKey, apiSecret, applicationId, privateKey }). Webhook signature validation prevents unauthorized requests from malicious actors impersonating Vonage, protects against replay attacks, and ensures webhook data integrity. Always implement signature verification in production environments before processing sensitive webhook data.
What Node.js version should I use for Vonage Messages API?
Use Node.js v18+ LTS (Long-Term Support) for Vonage Messages API projects as of October 2025. Node.js v18 provides stable async/await support, enhanced performance, modern JavaScript features (ES modules, top-level await), and security updates essential for production messaging applications. The @vonage/server-sdk officially supports Node.js v18+ and leverages its features for improved reliability. Avoid older versions (v14, v16) as they approach or have reached end-of-life with no security patches. Check your Node.js version with node --version and upgrade via nodejs.org or version managers like nvm (Node Version Manager). Use the same Node.js version in development and production to prevent compatibility issues.
How do I test Vonage two-way SMS without deploying to production?
Test Vonage two-way SMS locally using: (1) ngrok – expose your local Express server with ngrok http 3000, copy the HTTPS URL, and configure it in your Vonage Application webhook settings, (2) Manual testing – send SMS from your mobile phone to your Vonage number and verify inbound webhook logs, run your sending script and verify delivery on your phone, (3) ngrok Inspector – access http://127.0.0.1:4040 to inspect webhook payloads, headers, and JWT signatures in real-time, (4) Console logging – add comprehensive console.log() statements in webhook handlers to trace request flow, (5) Unit tests – use Jest or Mocha to test individual functions with mocked Vonage SDK calls, (6) Integration tests – send mock HTTP POST requests to webhook endpoints with valid JWT signatures and assert responses. Test edge cases including long messages (160+ characters), Unicode characters (emojis, non-Latin scripts), invalid E.164 formats, and webhook retry scenarios (non-200 responses).