Sending and Receiving SMS & WhatsApp with Node.js and Vonage
A guide to building a Node.js application using Express and the Vonage Messages API to handle SMS and WhatsApp messaging, including setup, sending, receiving via webhooks, and configuration.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send and receive both SMS and WhatsApp messages via the Vonage Messages API. We'll cover everything from project setup and core implementation to security, deployment, and testing considerations.
By the end of this tutorial, you will have a functional Node.js server capable of:
Sending outbound SMS messages using a dedicated Vonage number.
Sending outbound WhatsApp messages using the Vonage WhatsApp Sandbox.
Receiving inbound SMS and WhatsApp messages via webhooks.
Handling message status updates (e.g., delivery receipts) via webhooks.
This solution enables developers to integrate multi-channel messaging capabilities into their applications, facilitating communication with users on their preferred platforms.
Project overview and goals
What We'll Build: A Node.js application using the Express web framework that integrates with the Vonage Messages API to send and receive SMS and WhatsApp messages.
Problem Solved: This application provides a unified backend service to manage communication across two distinct channels (SMS and WhatsApp) through a single API provider (Vonage), simplifying development and maintenance.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications. Chosen for its asynchronous nature, large ecosystem (npm), and suitability for real-time applications like messaging handlers.
Express: A minimal and flexible Node.js web application framework. Chosen for its simplicity in setting up routes (webhooks) and middleware.
Vonage Messages API: A unified API for sending and receiving messages across various channels including SMS, MMS, WhatsApp, Facebook Messenger, and Viber. Chosen for its multi-channel support and developer-friendly SDKs.
@vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.
dotenv: A module to load environment variables from a .env file into process.env. Essential for securely managing API keys and configuration.
ngrok: A tool to expose local development servers to the internet. Crucial for testing Vonage webhooks during development.
System Architecture:
The system involves the following flow:
Outbound: Your Node.js/Express application sends an API request to the Vonage Messages API. Vonage then delivers the message to the user's phone via SMS or WhatsApp.
Inbound: The user sends a reply (SMS or WhatsApp) which goes through the Vonage network. Vonage sends a POST request containing the message data to your application's inbound webhook endpoint (e.g., /webhooks/inbound). Your application processes this data.
Status Updates: After sending a message, the Vonage Messages API sends status updates (e.g., 'delivered', 'failed') via POST requests to your application's status webhook endpoint (e.g., /webhooks/status). Your application processes these updates.
Prerequisites:
Node.js: Installed locally (Version 18 or higher recommended). Verify with node -v.
npm: Node Package Manager, installed with Node.js. Verify with npm -v.
Vonage API Account: Sign up for free at Vonage. You'll need your API Key and Secret.
Vonage Phone Number: Purchase an SMS-capable Vonage virtual number from your dashboard.
ngrok: Install ngrok and create a free account. This is necessary for testing webhooks locally.
WhatsApp Account: A personal WhatsApp account is needed to test with the Vonage Sandbox.
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory:
Open your terminal or command prompt and create a new directory for the project, then navigate into it.
Bash
mkdir vonage-node-messaging
cd vonage-node-messaging
Initialize npm:
Initialize the project using npm. The -y flag accepts the default settings.
Bash
npm init -y
This creates a package.json file.
Install Dependencies:
Install Express for the web server, the Vonage Server SDK, and dotenv for environment variable management.
Bash
npm install express @vonage/server-sdk dotenv
Create Project Structure:
Create the basic files and directories we'll need.
Bash
# On macOS / Linuxtouch index.js send.js .env .gitignore
# On Windows (Command Prompt)type nul > index.js
type nul > send.js
type nul > .envtype nul > .gitignore
# Or Windows (PowerShell)
New-Item index.js -ItemType File
New-Item send.js -ItemType File
New-Item .env -ItemType File
New-Item .gitignore -ItemType File
index.js: Our main Express server file for handling incoming webhooks.
send.js: A script to demonstrate sending outbound messages.
.env: Stores sensitive credentials and configuration.
.gitignore: Specifies intentionally untracked files that Git should ignore.
Configure .gitignore:
Add node_modules and .env to your .gitignore file to prevent committing dependencies and sensitive credentials.
Text
# .gitignore
node_modules/
.env
*.key # Also ignore private key files if stored locally
Set Up Environment Variables (.env):
Open the .env file and add the following variables. You will populate these with your actual credentials later.
.env
# .env
# Vonage API Credentials (Found on Vonage Dashboard homepage)
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application Settings (Created in Vonage Dashboard -> Applications)
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Path to your downloaded private key file
# Vonage Numbers
VONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_SMS_NUMBER # Your purchased Vonage number (e.g., 14155551212, no '+')
VONAGE_WHATSAPP_SANDBOX_NUMBER=14157386102 # The default Vonage WhatsApp Sandbox number (no '+')
# Target Number for Testing Sending
# Use E.164 format without leading '+' (e.g., 12015551212 for a US number)
YOUR_TARGET_PHONE_NUMBER=YOUR_PERSONAL_PHONE_NUMBER_E164
# Server Port (Optional - defaults to 3000 if not set)
PORT=3000
Explanation of Variables:
VONAGE_API_KEY, VONAGE_API_SECRET: Your primary credentials for authenticating API requests. Find these on the main page of your Vonage API Dashboard.
VONAGE_APPLICATION_ID: Unique ID for the Vonage Application you will create to handle messages.
VONAGE_PRIVATE_KEY_PATH: The file path to the private.key file downloaded when creating the Vonage Application. Authentication using Application ID and Private Key is required for the Messages API. Note: The path ./private.key assumes the key file is in the same directory from which you run the node index.js or node send.js command. Adjust the path accordingly if the file is located elsewhere relative to your execution context.
VONAGE_SMS_FROM_NUMBER: The Vonage virtual number you purchased and linked to your application. This is the sender ID for outbound SMS. Use E.164 format without the leading + (e.g., 14155551212).
VONAGE_WHATSAPP_SANDBOX_NUMBER: The shared Vonage number used by the WhatsApp Sandbox for testing (14157386102).
YOUR_TARGET_PHONE_NUMBER: Your personal mobile number (SMS and WhatsApp capable) used as the recipient for testing outbound messages. Use E.164 format without the leading +.
PORT: The port your local Express server will run on.
2. Implementing core functionality
We'll split the core logic into two parts: sending messages (send.js) and receiving messages (index.js).
Sending Outbound Messages (send.js)
This script demonstrates how to send both SMS and WhatsApp messages using the Vonage SDK.
Edit send.js:
Add the following code to send.js.
JavaScript
// send.jsrequire('dotenv').config(); // Load environment variables from .env fileconst { Vonage } = require('@vonage/server-sdk');
const { MESSAGES_SANDBOX_URL_MESSAGES } = require('@vonage/server-sdk/dist/enums/MessagesSandboxUrl'); // Needed for WhatsApp Sandbox// --- Configuration ---const vonageApiKey = process.env.VONAGE_API_KEY;
const vonageApiSecret = process.env.VONAGE_API_SECRET;
const vonageAppId = process.env.VONAGE_APPLICATION_ID;
const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH;
const vonageSmsFromNumber = process.env.VONAGE_SMS_FROM_NUMBER;
const vonageWhatsAppSandboxNumber = process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER;
const yourTargetNumber = process.env.YOUR_TARGET_PHONE_NUMBER;
// Basic validationif (!vonageApiKey || !vonageApiSecret || !vonageAppId || !privateKeyPath || !vonageSmsFromNumber || !yourTargetNumber) {
console.error('Error: Missing required environment variables. Please check your .env file.');
process.exit(1);
}
// --- Initialize Vonage Client ---// Authentication requires Application ID and Private Key for Messages APIconst vonage = newVonage({
apiKey: vonageApiKey,
apiSecret: vonageApiSecret, // Though not directly used by Messages API send, good practice to includeapplicationId: vonageAppId,
privateKey: privateKeyPath,
}, {
// **Important:** This `apiHost` directs WhatsApp traffic to the Sandbox.// Remove this option entirely when deploying for production WhatsApp// to use the default production Messages API endpoint.apiHost: MESSAGES_SANDBOX_URL_MESSAGES
});
// --- Send SMS Function ---asyncfunctionsendSms(to, text) {
console.log(`Attempting to send SMS to ${to}...`);
try {
// For SMS, the client defaults work fine (no sandbox override needed typically)// If the global client was configured with apiHost, you might need a separate client instance// or override the host per-call if the SDK supports it.// However, this example uses the same client, relying on channel='sms'const resp = await vonage.messages.send({
message_type: 'text',
text: text,
to: to,
from: vonageSmsFromNumber, // Your purchased Vonage SMS numberchannel: 'sms',
});
console.log(`SMS Sent Successfully: Message UUID: ${resp.message_uuid}`);
return resp;
} catch (err) {
console.error(`Error sending SMS to ${to}:`, err.response ? err.response.data : err.message);
throw err; // Re-throw the error for handling upstream if needed
}
}
// --- Send WhatsApp Message Function ---asyncfunctionsendWhatsAppMessage(to, text) {
console.log(`Attempting to send WhatsApp message to ${to} via Sandbox...`);
try {
// IMPORTANT: For WhatsApp Sandbox, 'from' must be the sandbox number.// The client initialized above is correctly configured with the Sandbox apiHost.const resp = await vonage.messages.send({
message_type: 'text',
text: text,
to: to, // Recipient's number (must be whitelisted in Sandbox)from: vonageWhatsAppSandboxNumber, // Vonage Sandbox numberchannel: 'whatsapp',
});
console.log(`WhatsApp Message Sent Successfully: Message UUID: ${resp.message_uuid}`);
return resp;
} catch (err) {
console.error(`Error sending WhatsApp message to ${to}:`, err.response ? err.response.data : err.message);
// Common Sandbox Error: Check if the 'to' number is whitelisted.if (err.response && err.response.data && err.response.data.title === 'Forbidden') {
console.error('Hint: Ensure the recipient number is whitelisted in the Vonage WhatsApp Sandbox.');
}
throw err; // Re-throw the error
}
}
// --- Main Execution Logic ---asyncfunctionmain() {
const messageText = `Hello from Vonage Node SDK! Sent at ${newDate().toLocaleTimeString()}`;
try {
// --- Send SMS Example ---awaitsendSms(yourTargetNumber, `[SMS] ${messageText}`);
// --- Send WhatsApp Example ---// Ensure yourTargetNumber has been whitelisted in the Sandbox first!awaitsendWhatsAppMessage(yourTargetNumber, `[WhatsApp] ${messageText}`);
console.log('\nFinished sending messages.');
} catch (error) {
console.error('\nAn error occurred during message sending.');
// Error details are logged within the functions
}
}
// --- Run the main function ---// We check if the script is run directlyif (require.main === module) {
main();
}
// Export functions if you want to require this file elsewheremodule.exports = { sendSms, sendWhatsAppMessage };
Explanation:
We load environment variables using dotenv.
We initialize the Vonage client using the Application ID and the path to the private key file.
Crucially, the apiHost option is set to MESSAGES_SANDBOX_URL_MESSAGES. This directs WhatsApp API calls to the sandbox endpoint. This apiHost line must be removed when deploying your application to send live WhatsApp messages.
sendSms: Uses vonage.messages.send with channel: 'sms' and your purchased VONAGE_SMS_FROM_NUMBER. Note that this uses the same client; ensure the sandbox apiHost doesn't interfere with SMS if you encounter issues (though typically it doesn't).
sendWhatsAppMessage: Uses vonage.messages.send with channel: 'whatsapp' and the VONAGE_WHATSAPP_SANDBOX_NUMBER as the from number. It includes error handling specific to the sandbox (e.g., whitelisting check).
The main function orchestrates sending both types of messages.
Error handling is included using try...catch blocks, logging detailed errors.
Receiving Inbound Messages & Status Updates (index.js)
This script sets up an Express server to listen for incoming webhook requests from Vonage.
Edit index.js:
Add the following code to index.js.
JavaScript
// index.jsrequire('dotenv').config(); // Load environment variablesconst express = require('express');
const { json, urlencoded } = express; // Import middleware directly// --- Configuration ---const port = process.env.PORT || 3000; // Use port from .env or default to 3000// --- Initialize Express App ---const app = express();
// --- Middleware ---// Parse JSON request bodies
app.use(json());
// Parse URL-encoded request bodies
app.use(urlencoded({ extended: true }));
// --- Webhook Endpoints ---// Endpoint for INBOUND messages (SMS/WhatsApp replies from users)
app.post('/webhooks/inbound', (req, res) => {
console.log('--- INBOUND MESSAGE RECEIVED ---');
console.log('Timestamp:', newDate().toISOString());
console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the entire payload// --- Basic Processing Example ---// You would typically store this message, trigger replies, etc. here.const messageType = req.body.message_type; // e.g., 'text', 'image'const channel = req.body.channel; // 'sms' or 'whatsapp'constfrom = req.body.from;
const to = req.body.to;
const messageContent = req.body.text || (req.body.image ? `[Image: ${req.body.image.url}]` : '[Unsupported Content]');
console.log(`\nParsed Info:`);
console.log(` Channel: ${channel}`);
console.log(` From: ${from}`);
console.log(` To: ${to}`);
console.log(` Type: ${messageType}`);
console.log(` Content: ${messageContent}`);
// --- IMPORTANT: Respond with 200 OK ---// Vonage webhooks expect a 200 OK response quickly.// Failure to respond or responding with an error will cause retries.
res.status(200).send('OK'); // Send a simple 'OK' or just .end()console.log('--- Inbound processing complete. Responded 200 OK. ---');
});
// Endpoint for MESSAGE STATUS updates (delivery receipts, etc.)
app.post('/webhooks/status', (req, res) => {
console.log('--- MESSAGE STATUS UPDATE RECEIVED ---');
console.log('Timestamp:', newDate().toISOString());
console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the entire payload// --- Basic Processing Example ---// Update message status in your database based on message_uuid and statusconst messageUuid = req.body.message_uuid;
const status = req.body.status; // e.g., 'delivered', 'read', 'failed'const timestamp = req.body.timestamp;
const channel = req.body.channel;
console.log(`\nParsed Info:`);
console.log(` Message UUID: ${messageUuid}`);
console.log(` Channel: ${channel}`);
console.log(` Status: ${status}`);
console.log(` Timestamp: ${timestamp}`);
if (req.body.error) {
console.error(' Error:', JSON.stringify(req.body.error, null, 2));
}
// --- IMPORTANT: Respond with 200 OK ---
res.status(200).send('OK');
console.log('--- Status processing complete. Responded 200 OK. ---');
});
// --- Health Check Endpoint (Good Practice) ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: newDate().toISOString() });
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Server listening for Vonage webhooks at http://localhost:${port}`);
console.log(`Webhook Endpoints:`);
console.log(` Inbound Messages: POST http://localhost:${port}/webhooks/inbound`);
console.log(` Message Status: POST http://localhost:${port}/webhooks/status`);
console.log(`Health Check: GET http://localhost:${port}/health`);
});
Explanation:
Loads environment variables and initializes Express.
Uses express.json() and express.urlencoded() middleware to parse incoming request bodies.
/webhooks/inbound: Handles POST requests from Vonage when a user sends a message to your Vonage number/WhatsApp Sandbox. It logs the payload and sends a 200 OK response. Crucially, Vonage requires a quick 200 OK response to prevent webhook retries.
/webhooks/status: Handles POST requests from Vonage about the status of messages you sent (e.g., delivered, failed, read). It also logs the payload and responds 200 OK.
/health: A simple GET endpoint to check if the server is running.
The server starts listening on the configured PORT.
3. Building a complete API layer
In this specific implementation, the API layer primarily consists of the webhook endpoints (/webhooks/inbound and /webhooks/status) that Vonage calls. The send.js script acts as a client initiating outbound messages.
For a more robust application, you might wrap the sending logic (sendSms, sendWhatsAppMessage) within authenticated API endpoints in your index.js server:
JavaScript
// Example: Adding a secure endpoint to send SMS (in index.js)// --- (Add authentication middleware first - e.g., check for API key in header) ---functionauthenticateRequest(req, res, next) {
const apiKey = req.headers['x-api-key'];
// WARNING: This is a highly insecure placeholder for illustration only.// In a real application, use a robust authentication strategy like// comparing against securely stored credentials or using JWT validation.if (apiKey && apiKey === 'YOUR_SECURE_INTERNAL_API_KEY') { // Replace with secure checknext(); // Allow request
} else {
res.status(401).json({ error: 'Unauthorized' });
}
}
// Import sending functions if refactored from send.js// const { sendSms, sendWhatsAppMessage } = require('./send'); // Assuming export// Example (add after webhook routes, before app.listen):/*
app.post('/api/send-sms', authenticateRequest, async (req, res) => {
const { to, text } = req.body;
// Basic validation
if (!to || !text) {
return res.status(400).json({ error: 'Missing ""to"" or ""text"" in request body' });
}
// Add more validation (e.g., phone number format)
try {
// Ensure sendSms is available in this scope (e.g., imported or defined here)
// You might need to instantiate the Vonage client within this scope too,
// or ensure the globally defined one is appropriate.
const result = await sendSms(to, text); // Assuming sendSms is defined/imported
res.status(200).json({ success: true, message_uuid: result.message_uuid });
} catch (error) {
// Log the error server-side
console.error('API Error sending SMS:', error);
res.status(500).json({ success: false, error: 'Failed to send SMS' });
}
});
// Add similar endpoint for /api/send-whatsapp
*/
Testing Endpoints (Webhook Simulation):
You can use curl or Postman to simulate Vonage calling your local webhook endpoints before exposing them with ngrok. Remember to replace ALL placeholders (like YOUR_VONAGE_SMS_NUMBER, SENDER_PHONE_NUMBER_E164, etc.) with actual or realistic mock values for the commands to work.
# Replace placeholders with valid values (UUID from actual sending, E.164 numbers without '+')
curl -X POST http://localhost:3000/webhooks/status \
-H ""Content-Type: application/json"" \
-d '{
""message_uuid"": ""actual-message-uuid-from-sending"",
""to"": ""YOUR_TARGET_PHONE_NUMBER_E164"",
""from"": ""VONAGE_WHATSAPP_SANDBOX_NUMBER"",
""channel"": ""whatsapp"",
""timestamp"": ""2025-04-20T10:05:00Z"",
""status"": ""delivered"",
""usage"": { ""currency"": ""EUR"", ""price"": ""0.0050"" }
}'
Check your index.js console output to verify the logs.
4. Integrating with Vonage (Dashboard Setup)
Now, let's configure Vonage to connect to our application.
Start ngrok:
Expose your local server (running on port 3000, or your configured PORT) to the internet.
Bash
# Make sure your Node.js server is running in another terminal: node index.js
ngrok http 3000
ngrok will provide a Forwarding URL ending in .ngrok-free.app or similar (e.g., https://abcd-1234-efgh.ngrok-free.app). Copy this HTTPS URL. This URL changes every time you restart ngrok (with the free plan).
Navigate to ""Applications"" -> ""Create a new application"".
Give it a name (e.g., My Node Messaging App).
Click ""Generate public and private key"". A private.key file will be downloaded. Save this file in your project's root directory (or update VONAGE_PRIVATE_KEY_PATH in .env if you save it elsewhere).
Crucially, enable the ""Messages"" capability.
Configure the Webhooks:
Inbound URL: Paste your ngrok HTTPS URL and append /webhooks/inbound. Example: https://abcd-1234-efgh.ngrok-free.app/webhooks/inbound
Status URL: Paste your ngrok HTTPS URL and append /webhooks/status. Example: https://abcd-1234-efgh.ngrok-free.app/webhooks/status
Click ""Generate new application"".
On the next screen, you'll see the Application ID. Copy this and paste it into your .env file as VONAGE_APPLICATION_ID.
Link Your Vonage SMS Number:
Still within the Application's settings page, scroll down to ""Link virtual numbers"".
Find the Vonage phone number you purchased for SMS and click ""Link"". This number should now appear under ""Linked Numbers"". Ensure it is SMS-capable.
Copy this number (in E.164 format, without the leading '+') and paste it into your .env file as VONAGE_SMS_FROM_NUMBER.
Set Up Vonage WhatsApp Sandbox:
In the Vonage Dashboard, navigate to ""Developer Tools"" -> ""Messages API Sandbox"".
You'll see a QR code and instructions to whitelist your personal WhatsApp number. Scan the QR code with your phone's camera or send the specified WhatsApp message from your phone to the Sandbox number (+14157386102). Follow the prompts from Vonage in WhatsApp to confirm. Note that this whitelisting typically expires after 24 hours, so you may need to re-whitelist for extended testing sessions.
Configure Sandbox Webhooks: On the same Sandbox page, find the ""Webhooks"" section.
Inbound URL: Enter your ngrok URL + /webhooks/inbound.
Status URL: Enter your ngrok URL + /webhooks/status.
Click ""Save webhooks"".
The VONAGE_WHATSAPP_SANDBOX_NUMBER in your .env should already be 14157386102.
Keep in mind that the Vonage WhatsApp Sandbox number (14157386102) is a shared testing number and cannot receive inbound calls.
Verify API Key/Secret:
Go back to the main Vonage Dashboard page.
Copy your ""API key"" and ""API secret"" and ensure they are correctly set as VONAGE_API_KEY and VONAGE_API_SECRET in your .env file.
Set Default SMS API:
Go to Dashboard -> Settings.
Scroll down to ""API keys"" section -> SMS settings.
Ensure ""Default SMS Setting"" is set to ""Messages API"". This ensures SMS sent/received use the Messages API format and webhooks configured in your Vonage Application, not the older SMS API. Click ""Save changes"".
5. Implementing error handling, logging, and retry mechanisms
Error Handling: We've added try...catch blocks in send.js around the vonage.messages.send calls. These catch errors during the API request (e.g., network issues, invalid credentials, invalid numbers). The error details (often found in err.response.data) are logged. In index.js, basic logging exists, but in production, you'd add try...catch within the webhook handlers for any complex processing logic that might fail.
Logging: We are using console.log and console.error. For production, use a structured logging library like Pino or Winston. This allows for leveled logging (debug, info, warn, error), JSON formatting (easier for log aggregation tools), and potentially sending logs to external services.
Retry Mechanisms (Vonage Webhooks): Vonage has a built-in retry mechanism for webhooks. If your /webhooks/inbound or /webhooks/status endpoint doesn't respond with a 200 OK status within a short timeout (typically a few seconds), Vonage will retry sending the webhook notification several times with increasing delays (exponential backoff).
Your Responsibility: Ensure your webhook handlers are fast and reliably return 200 OK. Offload any time-consuming processing (database writes, external API calls) to a background job queue (e.g., BullMQ, RabbitMQ) if necessary, so the webhook can respond immediately.
Idempotency: Design your webhook handlers to be idempotent – processing the same webhook payload multiple times should not cause duplicate actions or inconsistent state. Check if a message (using message_uuid) has already been processed before taking action.
6. Creating a database schema and data layer
This guide focuses on the core Vonage integration and uses console logging. In a real application, you'd store message data. Here's a conceptual outline:
Choice of Database: PostgreSQL, MongoDB, MySQL, or even a managed service like Airtable or Firebase Firestore.
Schema Design (Conceptual - e.g., PostgreSQL):
SQL
CREATE TABLE messages (
message_uuid VARCHAR(255) PRIMARY KEY, -- Vonage Message UUID
vonage_application_id VARCHAR(255), -- Your Vonage App ID
channel VARCHAR(50) NOT NULL, -- 'sms', 'whatsapp'
message_direction VARCHAR(10) NOT NULL, -- 'inbound', 'outbound'
sender_id VARCHAR(50) NOT NULL, -- Phone number (E.164 without '+') or WhatsApp ID
recipient_id VARCHAR(50) NOT NULL, -- Phone number (E.164 without '+')
message_type VARCHAR(50), -- 'text', 'image', 'audio', etc.
message_body TEXT, -- Text content
media_url VARCHAR(1024), -- URL for media content (if any)
status VARCHAR(50) DEFAULT'submitted', -- 'submitted', 'delivered', 'read', 'failed', 'accepted' (for inbound)
vonage_status_timestamp TIMESTAMPTZ, -- Timestamp from status webhook
inbound_received_at TIMESTAMPTZ, -- Timestamp when inbound message hit your server
outbound_created_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP, -- Timestamp when outbound message was initiated
error_code VARCHAR(100), -- Error code from status webhook (if failed)
error_reason TEXT -- Error reason from status webhook (if failed)
);
CREATE INDEX idx_messages_sender ON messages(sender_id);
CREATE INDEX idx_messages_recipient ON messages(recipient_id);
CREATE INDEX idx_messages_status ON messages(status);
CREATE INDEX idx_messages_channel ON messages(channel);
Data Layer Implementation: Use an Object-Relational Mapper (ORM) like Prisma or Sequelize (for SQL), or a driver like mongodb (for MongoDB) to interact with the database from your Node.js code.
Inbound (/webhooks/inbound): Parse req.body, create a new record in the messages table with message_direction='inbound', status='accepted', populate fields, and save.
Status (/webhooks/status): Parse req.body, find the message record by message_uuid, update its status, vonage_status_timestamp, and potentially error_code/error_reason.
Outbound (send.js or API endpoint): Before calling vonage.messages.send, create a record with message_direction='outbound', status='submitted'. After the API call succeeds, update the record with the returned message_uuid.
Migrations: Use the migration tools provided by your ORM (e.g., prisma migrate dev, sequelize db:migrate) to manage schema changes safely.
7. Adding security features
Secure Credentials: Never commit API keys, secrets, or private keys directly into code. Use environment variables (.env locally, secure configuration management in deployment). Ensure .env and *.key are in .gitignore.
Webhook Security (Signature Verification - Recommended): Vonage can sign webhook requests using a shared secret, allowing you to verify that requests genuinely originate from Vonage.
Find your ""Signature secret"" in the Vonage Dashboard -> Settings page (associated with your API Key). Add it to .env as VONAGE_SIGNATURE_SECRET.
Important: The implementation details for verifying the signature depend heavily on the current @vonage/server-sdk version and Vonage's specific signature scheme (e.g., JWT or header-based HMAC). Consult the official Vonage Node.js SDK documentation and developer portal for the current recommended method for signature verification within an Express application. You'll typically need middleware to perform this check before processing the webhook body.
Input Validation: Sanitize and validate all data received from webhooks (req.body) and any API inputs before processing or storing it. Check data types, lengths, formats (e.g., phone numbers), and escape output where necessary to prevent injection attacks (e.g., XSS if displaying message content).
Rate Limiting: Implement rate limiting on your API endpoints (like the example /api/send-sms) and potentially on webhook handlers if abuse is a concern, using libraries like express-rate-limit.
HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this for local testing). In production, ensure your server is configured for HTTPS using TLS certificates (e.g., via Let's Encrypt or your hosting provider).
Frequently asked questions
Use the Vonage Messages API with the Node.js SDK. After initializing the Vonage client with your API credentials, call vonage.messages.send with the recipient's number, your Vonage number as the sender, and the message text. Ensure your Vonage number is SMS-enabled in your dashboard.
The Vonage Messages API is a unified platform for sending and receiving messages across multiple channels, including SMS, WhatsApp, Viber, and Facebook Messenger. It simplifies multi-channel communication by providing a single API for various messaging platforms.
The Vonage WhatsApp Sandbox provides a testing environment for developing and experimenting with WhatsApp integrations without incurring real-world costs or needing full business approval. You can whitelist your own number for testing purposes.
Remove the apiHost: MESSAGES_SANDBOX_URL_MESSAGES configuration from your Vonage client only when deploying to production for live WhatsApp messaging. This setting is crucial for directing WhatsApp traffic to the Sandbox during development.
Yes, you can receive WhatsApp messages locally during development. Use ngrok to create a public, secure URL for your local server that Vonage can send webhooks to. Configure this ngrok URL in your Vonage application's webhook settings, specifically for the WhatsApp Sandbox.
In the Vonage Dashboard, create a new application and enable the Messages capability. Provide the URLs for your inbound and status webhooks, ensuring they point to your public server address (e.g., ngrok URL during development) appended with '/webhooks/inbound' and '/webhooks/status'. Link your Vonage virtual number to this application.
The Vonage Application ID uniquely identifies your application within the Vonage platform and is essential for authentication when using the Messages API with an associated private key. You'll need both the Application ID and the corresponding private key to initialize the Vonage client in your Node.js code.
Vonage automatically retries webhook requests if your server doesn't respond with a 200 OK within a short timeframe. Ensure your webhook handlers execute quickly and return 200 OK. Process time-consuming tasks asynchronously using background jobs. Implement idempotency checks (e.g., using message_uuid) to prevent duplicate actions on retries.
ngrok creates secure tunnels from public URLs to your local machine, allowing Vonage to send webhooks to your development server. This is crucial for testing webhooks before deploying your application.
Store API keys, secrets, and private keys in environment variables (.env file locally) and never commit them to version control. Use secure configuration management systems in production.
Go to the Messages API Sandbox in your Vonage Dashboard. Scan the QR code with your WhatsApp app, or send the specified message from your phone's WhatsApp to the provided number. Follow the subsequent instructions to complete the whitelisting.
A frequent error is "Forbidden," indicating the recipient isn't whitelisted. Ensure the target number is correctly added to the Sandbox. Other errors might relate to incorrect apiHost settings or using a non-sandbox number as the 'from' address in WhatsApp messages.
A recommended schema includes fields for message_uuid, channel, direction, sender/recipient IDs, message content, status, timestamps, and error details. Consider indexing columns like sender/recipient, status, and channel for efficient querying.
Status updates provide real-time delivery information about your outbound messages (e.g., 'delivered', 'failed', 'read'). Vonage sends these updates to your status webhook URL, where your application can update the message status in its database accordingly.
Enable signature verification in your Vonage Dashboard settings to enhance security. Refer to the Vonage Node.js SDK documentation for the latest implementation details, which often involve middleware using your signature secret to validate the request's authenticity.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send and receive both SMS and WhatsApp messages via the Vonage Messages API. We'll cover everything from project setup and core implementation to security, deployment, and testing considerations.
By the end of this tutorial, you will have a functional Node.js server capable of:
This solution enables developers to integrate multi-channel messaging capabilities into their applications, facilitating communication with users on their preferred platforms.
Project overview and goals
What We'll Build: A Node.js application using the Express web framework that integrates with the Vonage Messages API to send and receive SMS and WhatsApp messages.
Problem Solved: This application provides a unified backend service to manage communication across two distinct channels (SMS and WhatsApp) through a single API provider (Vonage), simplifying development and maintenance.
Technologies Used:
@vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.dotenv: A module to load environment variables from a.envfile intoprocess.env. Essential for securely managing API keys and configuration.ngrok: A tool to expose local development servers to the internet. Crucial for testing Vonage webhooks during development.System Architecture:
The system involves the following flow:
/webhooks/inbound). Your application processes this data./webhooks/status). Your application processes these updates.Prerequisites:
node -v.npm -v.ngrok: Installngrokand create a free account. This is necessary for testing webhooks locally.1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for the project, then navigate into it.
mkdir vonage-node-messaging cd vonage-node-messagingInitialize npm: Initialize the project using npm. The
-yflag accepts the default settings.This creates a
package.jsonfile.Install Dependencies: Install Express for the web server, the Vonage Server SDK, and
dotenvfor environment variable management.Create Project Structure: Create the basic files and directories we'll need.
# On macOS / Linux touch index.js send.js .env .gitignore # On Windows (Command Prompt) type nul > index.js type nul > send.js type nul > .env type nul > .gitignore # Or Windows (PowerShell) New-Item index.js -ItemType File New-Item send.js -ItemType File New-Item .env -ItemType File New-Item .gitignore -ItemType Fileindex.js: Our main Express server file for handling incoming webhooks.send.js: A script to demonstrate sending outbound messages..env: Stores sensitive credentials and configuration..gitignore: Specifies intentionally untracked files that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing dependencies and sensitive credentials.Set Up Environment Variables (
.env): Open the.envfile and add the following variables. You will populate these with your actual credentials later.Explanation of Variables:
VONAGE_API_KEY,VONAGE_API_SECRET: Your primary credentials for authenticating API requests. Find these on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Unique ID for the Vonage Application you will create to handle messages.VONAGE_PRIVATE_KEY_PATH: The file path to theprivate.keyfile downloaded when creating the Vonage Application. Authentication using Application ID and Private Key is required for the Messages API. Note: The path./private.keyassumes the key file is in the same directory from which you run thenode index.jsornode send.jscommand. Adjust the path accordingly if the file is located elsewhere relative to your execution context.VONAGE_SMS_FROM_NUMBER: The Vonage virtual number you purchased and linked to your application. This is the sender ID for outbound SMS. Use E.164 format without the leading+(e.g.,14155551212).VONAGE_WHATSAPP_SANDBOX_NUMBER: The shared Vonage number used by the WhatsApp Sandbox for testing (14157386102).YOUR_TARGET_PHONE_NUMBER: Your personal mobile number (SMS and WhatsApp capable) used as the recipient for testing outbound messages. Use E.164 format without the leading+.PORT: The port your local Express server will run on.2. Implementing core functionality
We'll split the core logic into two parts: sending messages (
send.js) and receiving messages (index.js).Sending Outbound Messages (
send.js)This script demonstrates how to send both SMS and WhatsApp messages using the Vonage SDK.
Edit
send.js: Add the following code tosend.js.// send.js require('dotenv').config(); // Load environment variables from .env file const { Vonage } = require('@vonage/server-sdk'); const { MESSAGES_SANDBOX_URL_MESSAGES } = require('@vonage/server-sdk/dist/enums/MessagesSandboxUrl'); // Needed for WhatsApp Sandbox // --- Configuration --- const vonageApiKey = process.env.VONAGE_API_KEY; const vonageApiSecret = process.env.VONAGE_API_SECRET; const vonageAppId = process.env.VONAGE_APPLICATION_ID; const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH; const vonageSmsFromNumber = process.env.VONAGE_SMS_FROM_NUMBER; const vonageWhatsAppSandboxNumber = process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER; const yourTargetNumber = process.env.YOUR_TARGET_PHONE_NUMBER; // Basic validation if (!vonageApiKey || !vonageApiSecret || !vonageAppId || !privateKeyPath || !vonageSmsFromNumber || !yourTargetNumber) { console.error('Error: Missing required environment variables. Please check your .env file.'); process.exit(1); } // --- Initialize Vonage Client --- // Authentication requires Application ID and Private Key for Messages API const vonage = new Vonage({ apiKey: vonageApiKey, apiSecret: vonageApiSecret, // Though not directly used by Messages API send, good practice to include applicationId: vonageAppId, privateKey: privateKeyPath, }, { // **Important:** This `apiHost` directs WhatsApp traffic to the Sandbox. // Remove this option entirely when deploying for production WhatsApp // to use the default production Messages API endpoint. apiHost: MESSAGES_SANDBOX_URL_MESSAGES }); // --- Send SMS Function --- async function sendSms(to, text) { console.log(`Attempting to send SMS to ${to}...`); try { // For SMS, the client defaults work fine (no sandbox override needed typically) // If the global client was configured with apiHost, you might need a separate client instance // or override the host per-call if the SDK supports it. // However, this example uses the same client, relying on channel='sms' const resp = await vonage.messages.send({ message_type: 'text', text: text, to: to, from: vonageSmsFromNumber, // Your purchased Vonage SMS number channel: 'sms', }); console.log(`SMS Sent Successfully: Message UUID: ${resp.message_uuid}`); return resp; } catch (err) { console.error(`Error sending SMS to ${to}:`, err.response ? err.response.data : err.message); throw err; // Re-throw the error for handling upstream if needed } } // --- Send WhatsApp Message Function --- async function sendWhatsAppMessage(to, text) { console.log(`Attempting to send WhatsApp message to ${to} via Sandbox...`); try { // IMPORTANT: For WhatsApp Sandbox, 'from' must be the sandbox number. // The client initialized above is correctly configured with the Sandbox apiHost. const resp = await vonage.messages.send({ message_type: 'text', text: text, to: to, // Recipient's number (must be whitelisted in Sandbox) from: vonageWhatsAppSandboxNumber, // Vonage Sandbox number channel: 'whatsapp', }); console.log(`WhatsApp Message Sent Successfully: Message UUID: ${resp.message_uuid}`); return resp; } catch (err) { console.error(`Error sending WhatsApp message to ${to}:`, err.response ? err.response.data : err.message); // Common Sandbox Error: Check if the 'to' number is whitelisted. if (err.response && err.response.data && err.response.data.title === 'Forbidden') { console.error('Hint: Ensure the recipient number is whitelisted in the Vonage WhatsApp Sandbox.'); } throw err; // Re-throw the error } } // --- Main Execution Logic --- async function main() { const messageText = `Hello from Vonage Node SDK! Sent at ${new Date().toLocaleTimeString()}`; try { // --- Send SMS Example --- await sendSms(yourTargetNumber, `[SMS] ${messageText}`); // --- Send WhatsApp Example --- // Ensure yourTargetNumber has been whitelisted in the Sandbox first! await sendWhatsAppMessage(yourTargetNumber, `[WhatsApp] ${messageText}`); console.log('\nFinished sending messages.'); } catch (error) { console.error('\nAn error occurred during message sending.'); // Error details are logged within the functions } } // --- Run the main function --- // We check if the script is run directly if (require.main === module) { main(); } // Export functions if you want to require this file elsewhere module.exports = { sendSms, sendWhatsAppMessage };Explanation:
dotenv.Vonageclient using the Application ID and the path to the private key file.apiHostoption is set toMESSAGES_SANDBOX_URL_MESSAGES. This directs WhatsApp API calls to the sandbox endpoint. ThisapiHostline must be removed when deploying your application to send live WhatsApp messages.sendSms: Usesvonage.messages.sendwithchannel: 'sms'and your purchasedVONAGE_SMS_FROM_NUMBER. Note that this uses the same client; ensure the sandboxapiHostdoesn't interfere with SMS if you encounter issues (though typically it doesn't).sendWhatsAppMessage: Usesvonage.messages.sendwithchannel: 'whatsapp'and theVONAGE_WHATSAPP_SANDBOX_NUMBERas thefromnumber. It includes error handling specific to the sandbox (e.g., whitelisting check).mainfunction orchestrates sending both types of messages.try...catchblocks, logging detailed errors.Receiving Inbound Messages & Status Updates (
index.js)This script sets up an Express server to listen for incoming webhook requests from Vonage.
Edit
index.js: Add the following code toindex.js.// index.js require('dotenv').config(); // Load environment variables const express = require('express'); const { json, urlencoded } = express; // Import middleware directly // --- Configuration --- const port = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Initialize Express App --- const app = express(); // --- Middleware --- // Parse JSON request bodies app.use(json()); // Parse URL-encoded request bodies app.use(urlencoded({ extended: true })); // --- Webhook Endpoints --- // Endpoint for INBOUND messages (SMS/WhatsApp replies from users) app.post('/webhooks/inbound', (req, res) => { console.log('--- INBOUND MESSAGE RECEIVED ---'); console.log('Timestamp:', new Date().toISOString()); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the entire payload // --- Basic Processing Example --- // You would typically store this message, trigger replies, etc. here. const messageType = req.body.message_type; // e.g., 'text', 'image' const channel = req.body.channel; // 'sms' or 'whatsapp' const from = req.body.from; const to = req.body.to; const messageContent = req.body.text || (req.body.image ? `[Image: ${req.body.image.url}]` : '[Unsupported Content]'); console.log(`\nParsed Info:`); console.log(` Channel: ${channel}`); console.log(` From: ${from}`); console.log(` To: ${to}`); console.log(` Type: ${messageType}`); console.log(` Content: ${messageContent}`); // --- IMPORTANT: Respond with 200 OK --- // Vonage webhooks expect a 200 OK response quickly. // Failure to respond or responding with an error will cause retries. res.status(200).send('OK'); // Send a simple 'OK' or just .end() console.log('--- Inbound processing complete. Responded 200 OK. ---'); }); // Endpoint for MESSAGE STATUS updates (delivery receipts, etc.) app.post('/webhooks/status', (req, res) => { console.log('--- MESSAGE STATUS UPDATE RECEIVED ---'); console.log('Timestamp:', new Date().toISOString()); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the entire payload // --- Basic Processing Example --- // Update message status in your database based on message_uuid and status const messageUuid = req.body.message_uuid; const status = req.body.status; // e.g., 'delivered', 'read', 'failed' const timestamp = req.body.timestamp; const channel = req.body.channel; console.log(`\nParsed Info:`); console.log(` Message UUID: ${messageUuid}`); console.log(` Channel: ${channel}`); console.log(` Status: ${status}`); console.log(` Timestamp: ${timestamp}`); if (req.body.error) { console.error(' Error:', JSON.stringify(req.body.error, null, 2)); } // --- IMPORTANT: Respond with 200 OK --- res.status(200).send('OK'); console.log('--- Status processing complete. Responded 200 OK. ---'); }); // --- Health Check Endpoint (Good Practice) --- app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening for Vonage webhooks at http://localhost:${port}`); console.log(`Webhook Endpoints:`); console.log(` Inbound Messages: POST http://localhost:${port}/webhooks/inbound`); console.log(` Message Status: POST http://localhost:${port}/webhooks/status`); console.log(`Health Check: GET http://localhost:${port}/health`); });Explanation:
express.json()andexpress.urlencoded()middleware to parse incoming request bodies./webhooks/inbound: Handles POST requests from Vonage when a user sends a message to your Vonage number/WhatsApp Sandbox. It logs the payload and sends a200 OKresponse. Crucially, Vonage requires a quick200 OKresponse to prevent webhook retries./webhooks/status: Handles POST requests from Vonage about the status of messages you sent (e.g., delivered, failed, read). It also logs the payload and responds200 OK./health: A simple GET endpoint to check if the server is running.PORT.3. Building a complete API layer
In this specific implementation, the API layer primarily consists of the webhook endpoints (
/webhooks/inboundand/webhooks/status) that Vonage calls. Thesend.jsscript acts as a client initiating outbound messages.For a more robust application, you might wrap the sending logic (
sendSms,sendWhatsAppMessage) within authenticated API endpoints in yourindex.jsserver:// Example: Adding a secure endpoint to send SMS (in index.js) // --- (Add authentication middleware first - e.g., check for API key in header) --- function authenticateRequest(req, res, next) { const apiKey = req.headers['x-api-key']; // WARNING: This is a highly insecure placeholder for illustration only. // In a real application, use a robust authentication strategy like // comparing against securely stored credentials or using JWT validation. if (apiKey && apiKey === 'YOUR_SECURE_INTERNAL_API_KEY') { // Replace with secure check next(); // Allow request } else { res.status(401).json({ error: 'Unauthorized' }); } } // Import sending functions if refactored from send.js // const { sendSms, sendWhatsAppMessage } = require('./send'); // Assuming export // Example (add after webhook routes, before app.listen): /* app.post('/api/send-sms', authenticateRequest, async (req, res) => { const { to, text } = req.body; // Basic validation if (!to || !text) { return res.status(400).json({ error: 'Missing ""to"" or ""text"" in request body' }); } // Add more validation (e.g., phone number format) try { // Ensure sendSms is available in this scope (e.g., imported or defined here) // You might need to instantiate the Vonage client within this scope too, // or ensure the globally defined one is appropriate. const result = await sendSms(to, text); // Assuming sendSms is defined/imported res.status(200).json({ success: true, message_uuid: result.message_uuid }); } catch (error) { // Log the error server-side console.error('API Error sending SMS:', error); res.status(500).json({ success: false, error: 'Failed to send SMS' }); } }); // Add similar endpoint for /api/send-whatsapp */Testing Endpoints (Webhook Simulation): You can use
curlor Postman to simulate Vonage calling your local webhook endpoints before exposing them withngrok. Remember to replace ALL placeholders (likeYOUR_VONAGE_SMS_NUMBER,SENDER_PHONE_NUMBER_E164, etc.) with actual or realistic mock values for the commands to work.Simulate Inbound SMS:
# Replace placeholders with valid E.164 numbers (without '+') curl -X POST http://localhost:3000/webhooks/inbound \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""YOUR_VONAGE_SMS_NUMBER"", ""from"": ""SENDER_PHONE_NUMBER_E164"", ""channel"": ""sms"", ""message_uuid"": ""mock-sms-uuid-123"", ""timestamp"": ""2025-04-20T10:00:00Z"", ""message_type"": ""text"", ""text"": ""Hello from curl test!"", ""sms"": { ""num_messages"": ""1"" } }'Simulate WhatsApp Status Update:
# Replace placeholders with valid values (UUID from actual sending, E.164 numbers without '+') curl -X POST http://localhost:3000/webhooks/status \ -H ""Content-Type: application/json"" \ -d '{ ""message_uuid"": ""actual-message-uuid-from-sending"", ""to"": ""YOUR_TARGET_PHONE_NUMBER_E164"", ""from"": ""VONAGE_WHATSAPP_SANDBOX_NUMBER"", ""channel"": ""whatsapp"", ""timestamp"": ""2025-04-20T10:05:00Z"", ""status"": ""delivered"", ""usage"": { ""currency"": ""EUR"", ""price"": ""0.0050"" } }'Check your
index.jsconsole output to verify the logs.4. Integrating with Vonage (Dashboard Setup)
Now, let's configure Vonage to connect to our application.
Start
ngrok: Expose your local server (running on port 3000, or your configuredPORT) to the internet.# Make sure your Node.js server is running in another terminal: node index.js ngrok http 3000ngrokwill provide aForwardingURL ending in.ngrok-free.appor similar (e.g.,https://abcd-1234-efgh.ngrok-free.app). Copy this HTTPS URL. This URL changes every time you restartngrok(with the free plan).Create a Vonage Application:
My Node Messaging App).private.keyfile will be downloaded. Save this file in your project's root directory (or updateVONAGE_PRIVATE_KEY_PATHin.envif you save it elsewhere).ngrokHTTPS URL and append/webhooks/inbound. Example:https://abcd-1234-efgh.ngrok-free.app/webhooks/inboundngrokHTTPS URL and append/webhooks/status. Example:https://abcd-1234-efgh.ngrok-free.app/webhooks/status.envfile asVONAGE_APPLICATION_ID.Link Your Vonage SMS Number:
.envfile asVONAGE_SMS_FROM_NUMBER.Set Up Vonage WhatsApp Sandbox:
+14157386102). Follow the prompts from Vonage in WhatsApp to confirm. Note that this whitelisting typically expires after 24 hours, so you may need to re-whitelist for extended testing sessions.ngrokURL +/webhooks/inbound.ngrokURL +/webhooks/status.VONAGE_WHATSAPP_SANDBOX_NUMBERin your.envshould already be14157386102.14157386102) is a shared testing number and cannot receive inbound calls.Verify API Key/Secret:
VONAGE_API_KEYandVONAGE_API_SECRETin your.envfile.Set Default SMS API:
5. Implementing error handling, logging, and retry mechanisms
try...catchblocks insend.jsaround thevonage.messages.sendcalls. These catch errors during the API request (e.g., network issues, invalid credentials, invalid numbers). The error details (often found inerr.response.data) are logged. Inindex.js, basic logging exists, but in production, you'd addtry...catchwithin the webhook handlers for any complex processing logic that might fail.console.logandconsole.error. For production, use a structured logging library like Pino or Winston. This allows for leveled logging (debug, info, warn, error), JSON formatting (easier for log aggregation tools), and potentially sending logs to external services.// npm install pino const pino = require('pino')(); // ... inside webhook handler pino.info({ body: req.body }, 'Inbound message received'); // ... inside catch block pino.error({ err: error, body: req.body }, 'Error processing inbound message');/webhooks/inboundor/webhooks/statusendpoint doesn't respond with a200 OKstatus within a short timeout (typically a few seconds), Vonage will retry sending the webhook notification several times with increasing delays (exponential backoff).200 OK. Offload any time-consuming processing (database writes, external API calls) to a background job queue (e.g., BullMQ, RabbitMQ) if necessary, so the webhook can respond immediately.message_uuid) has already been processed before taking action.6. Creating a database schema and data layer
This guide focuses on the core Vonage integration and uses console logging. In a real application, you'd store message data. Here's a conceptual outline:
Choice of Database: PostgreSQL, MongoDB, MySQL, or even a managed service like Airtable or Firebase Firestore.
Schema Design (Conceptual - e.g., PostgreSQL):
CREATE TABLE messages ( message_uuid VARCHAR(255) PRIMARY KEY, -- Vonage Message UUID vonage_application_id VARCHAR(255), -- Your Vonage App ID channel VARCHAR(50) NOT NULL, -- 'sms', 'whatsapp' message_direction VARCHAR(10) NOT NULL, -- 'inbound', 'outbound' sender_id VARCHAR(50) NOT NULL, -- Phone number (E.164 without '+') or WhatsApp ID recipient_id VARCHAR(50) NOT NULL, -- Phone number (E.164 without '+') message_type VARCHAR(50), -- 'text', 'image', 'audio', etc. message_body TEXT, -- Text content media_url VARCHAR(1024), -- URL for media content (if any) status VARCHAR(50) DEFAULT 'submitted', -- 'submitted', 'delivered', 'read', 'failed', 'accepted' (for inbound) vonage_status_timestamp TIMESTAMPTZ, -- Timestamp from status webhook inbound_received_at TIMESTAMPTZ, -- Timestamp when inbound message hit your server outbound_created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, -- Timestamp when outbound message was initiated error_code VARCHAR(100), -- Error code from status webhook (if failed) error_reason TEXT -- Error reason from status webhook (if failed) ); CREATE INDEX idx_messages_sender ON messages(sender_id); CREATE INDEX idx_messages_recipient ON messages(recipient_id); CREATE INDEX idx_messages_status ON messages(status); CREATE INDEX idx_messages_channel ON messages(channel);Data Layer Implementation: Use an Object-Relational Mapper (ORM) like Prisma or Sequelize (for SQL), or a driver like
mongodb(for MongoDB) to interact with the database from your Node.js code./webhooks/inbound): Parsereq.body, create a new record in themessagestable withmessage_direction='inbound',status='accepted', populate fields, and save./webhooks/status): Parsereq.body, find the message record bymessage_uuid, update itsstatus,vonage_status_timestamp, and potentiallyerror_code/error_reason.send.jsor API endpoint): Before callingvonage.messages.send, create a record withmessage_direction='outbound',status='submitted'. After the API call succeeds, update the record with the returnedmessage_uuid.Migrations: Use the migration tools provided by your ORM (e.g.,
prisma migrate dev,sequelize db:migrate) to manage schema changes safely.7. Adding security features
.envlocally, secure configuration management in deployment). Ensure.envand*.keyare in.gitignore..envasVONAGE_SIGNATURE_SECRET.@vonage/server-sdkversion and Vonage's specific signature scheme (e.g., JWT or header-based HMAC). Consult the official Vonage Node.js SDK documentation and developer portal for the current recommended method for signature verification within an Express application. You'll typically need middleware to perform this check before processing the webhook body.req.body) and any API inputs before processing or storing it. Check data types, lengths, formats (e.g., phone numbers), and escape output where necessary to prevent injection attacks (e.g., XSS if displaying message content)./api/send-sms) and potentially on webhook handlers if abuse is a concern, using libraries likeexpress-rate-limit.ngrokprovides this for local testing). In production, ensure your server is configured for HTTPS using TLS certificates (e.g., via Let's Encrypt or your hosting provider).