Send and receive SMS & WhatsApp messages with Node.js, Express, and Vonage
This guide provides a comprehensive 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 will cover everything from initial project setup and Vonage configuration to implementing core messaging logic, handling webhooks securely, and providing troubleshooting tips.
By the end of this tutorial, you will have a functional application capable of:
- Sending outbound SMS messages programmatically.
- Receiving inbound SMS messages via webhooks.
- Sending outbound WhatsApp messages using the Vonage WhatsApp Sandbox.
- Receiving inbound WhatsApp messages via webhooks and sending automated replies.
This guide solves the common need for applications to integrate multi-channel messaging capabilities for notifications, alerts, customer support, or two-factor authentication, leveraging the unified Vonage Messages API.
Technologies Used:
-
Node.js: A JavaScript runtime environment for building server-side applications.
-
Express: A minimal and flexible Node.js web application framework.
-
Vonage Messages API: A unified API for sending and receiving messages across various channels (SMS, MMS, WhatsApp, Facebook Messenger, Viber).
-
Vonage Node.js SDK: Simplifies interaction with Vonage APIs in Node.js.
-
ngrok: A tool to expose local development servers to the internet for webhook testing.
-
dotenv: A module to load environment variables from a
.env
file. -
Outbound: Your Node.js app makes API calls to Vonage, which then delivers the message via the appropriate network (SMS/WhatsApp).
-
Inbound: A user sends a message (SMS/WhatsApp) to your Vonage number/WhatsApp Sandbox number. Vonage receives it and forwards the message data to your application's configured webhook URLs via an HTTP
POST
request.
Prerequisites:
- A Vonage API account. Sign up here if you don't have one.
- Node.js installed (v18 or higher recommended). Download here.
- An ngrok account (free tier is sufficient) and the ngrok CLI installed. Sign up and download here.
- A phone number capable of sending/receiving SMS for testing.
- A WhatsApp-enabled phone number for testing the WhatsApp integration.
Final Outcome:
You will have two main scripts:
index.js
: A standalone script to send an outbound SMS message.server.js
: An Express server that listens for incoming SMS and WhatsApp messages via webhooks, logs them, and automatically replies to incoming WhatsApp messages.
1. Setting up the project
Let's start by creating our project directory and initializing it as a Node.js project.
1. Create Project Directory: Open your terminal or command prompt and create a new directory for your project.
mkdir vonage-node-messaging
cd vonage-node-messaging
2. Initialize Node.js Project:
Initialize the project using npm, which creates a package.json
file.
npm init -y
The -y
flag accepts the default settings.
3. Install Dependencies: We need several packages:
express
: The web framework.@vonage/server-sdk
: The core Vonage SDK.@vonage/messages
: Specific helpers for the Messages API (likeWhatsAppText
).@vonage/jwt
: For verifying webhook signatures (important for security).dotenv
: To manage environment variables securely.
Install them using npm:
npm install express @vonage/server-sdk @vonage/messages @vonage/jwt dotenv
4. Create Project Structure: For simplicity, we'll keep our code in the root directory for this example. Create the necessary files:
touch index.js server.js .env .gitignore
index.js
: Will contain the code to send an SMS.server.js
: Will contain the Express server to handle incoming webhooks..env
: Will store our API keys and other configuration secrets. Never commit this file to version control..gitignore
: Specifies intentionally untracked files that Git should ignore.
5. Configure .gitignore
:
Add node_modules
and .env
to your .gitignore
file to prevent committing dependencies and secrets.
# .gitignore
node_modules/
.env
6. Set up Environment Variables (.env
):
Open the .env
file and add the following placeholders. We will fill these in during the Vonage configuration steps.
# .env
# Vonage API Credentials (Found on Vonage Dashboard homepage)
VONAGE_API_KEY=
VONAGE_API_SECRET=
# Vonage Application Details (Generated when creating a Vonage Application)
VONAGE_APPLICATION_ID=
VONAGE_PRIVATE_KEY=./private.key # Path to your downloaded private key file
# Vonage Numbers (One purchased Vonage number for SMS, Sandbox number for WhatsApp)
VONAGE_SMS_FROM_NUMBER= # Your Vonage virtual number for sending SMS
VONAGE_WHATSAPP_NUMBER=14157386102 # Vonage Sandbox number (fixed)
# Webhook Signature Verification (Found in Vonage Dashboard Settings)
VONAGE_API_SIGNATURE_SECRET=
# Server Configuration
PORT=8000 # Port for the Express server to listen on
Explanation of Configuration Choices:
.env
: Using environment variables keeps sensitive credentials out of the codebase, enhancing security and making configuration easier across different environments (development, production)..gitignore
: Essential for preventing accidental exposure of secrets (.env
) and unnecessary bloating of the repository (node_modules
).- Express: Chosen for its simplicity, extensive middleware ecosystem, and widespread adoption in the Node.js community, making it ideal for building webhook handlers.
2. Integrating with Vonage (Configuration)
Before writing code, we need to configure Vonage by creating an Application, setting up the WhatsApp Sandbox, and obtaining the necessary credentials.
1. Obtain API Key and Secret:
- Log in to your Vonage API Dashboard.
- Your
API key
andAPI secret
are displayed prominently on the homepage. - Copy these values and paste them into your
.env
file forVONAGE_API_KEY
andVONAGE_API_SECRET
.
2. Create a Vonage Application: Vonage Applications act as containers for your communication settings, including webhook URLs and authentication keys.
- Navigate to ""Applications"" in the left-hand menu, then click ""Create a new application"".
- Give your application a name (e.g., ""Node Messaging App"").
- Generate Public/Private Key Pair: Click the ""Generate public and private key"" button. This will automatically download a
private.key
file. Save this file in the root of your project directory (vonage-node-messaging
). The public key is stored by Vonage. Update theVONAGE_PRIVATE_KEY
path in your.env
file if you save it elsewhere (the default./private.key
assumes it's in the root). - Enable Capabilities: Toggle on the ""Messages"" capability.
- Configure Webhook URLs: For now, enter placeholder URLs. We will update these later with our ngrok URL.
- Inbound URL:
http://example.com/webhooks/inbound
- Status URL:
http://example.com/webhooks/status
- Inbound URL:
- Click ""Generate new application"".
- On the next page, you'll see your Application ID. Copy this ID and paste it into your
.env
file forVONAGE_APPLICATION_ID
.
3. Link a Vonage Number (for SMS): To send and receive SMS, you need a Vonage virtual number linked to your application.
- If you don't have a number, navigate to ""Numbers"" > ""Buy numbers"" and purchase an SMS-capable number in your desired country.
- Go back to your Application settings (""Applications"" > click your app name).
- Scroll down to the ""Link virtual numbers"" section. Click ""Link"" next to the number you want to use for SMS.
- Copy this Vonage number (including the country code, e.g.,
12015550123
) and paste it into your.env
file forVONAGE_SMS_FROM_NUMBER
.
4. Set up the Messages API WhatsApp Sandbox: The Sandbox provides a testing environment without needing a full WhatsApp Business Account initially.
- Navigate to ""Messages API Sandbox"" in the left-hand menu.
- You'll see instructions to add the Vonage Sandbox number (
+14157386102
) to your WhatsApp contacts and send it a specific join message (e.g., ""WhatsApp Sandbox""). Do this now from the phone number you want to use for testing. This ""allowlists"" your number. - The Sandbox number (
14157386102
) is pre-filled in the.env
file forVONAGE_WHATSAPP_NUMBER
. - Configure Sandbox Webhooks: Similar to the application, we need to set webhooks for the Sandbox. Enter placeholders for now:
- Inbound URL:
http://example.com/webhooks/inbound
- Status URL:
http://example.com/webhooks/status
- Inbound URL:
- Click ""Save webhooks"".
5. Obtain API Signature Secret (for Webhook Security): Vonage signs incoming webhook requests with a JWT (JSON Web Token) using a secret key, allowing you to verify their authenticity.
- Click your name in the top-right corner of the dashboard, then ""Settings"".
- Under ""API settings"", find the ""Default SMS Setting"". While this guide explicitly uses the Messages API for sending (making this setting less critical for the sending code shown), ensuring it's set to ""Messages API"" aligns your account settings with the API being used.
- Scroll down to the ""Signing secret"" section. You may need to click ""Generate new secret"" if one doesn't exist.
- Copy the Signature Secret.
- Paste this value into your
.env
file forVONAGE_API_SIGNATURE_SECRET
.
6. Start ngrok: To receive webhooks on your local machine, we need to expose your local server to the internet using ngrok.
- Open a new terminal window (keep it running).
- Start ngrok, telling it to forward traffic to the port your Express server will run on (defined as
8000
in our.env
).
ngrok http 8000
- ngrok will display a public ""Forwarding"" URL (e.g.,
https://<unique-subdomain>.ngrok-free.app
). Copy this HTTPS URL.
7. Update Webhook URLs in Vonage: Now, replace the placeholder URLs with your actual ngrok URL.
- Application Webhooks:
- Go back to ""Applications"" > your application > ""Edit"".
- Update the Messages capability URLs:
- Inbound URL:
<your-ngrok-https-url>/webhooks/inbound
- Status URL:
<your-ngrok-https-url>/webhooks/status
- Inbound URL:
- Click ""Save changes"".
- Sandbox Webhooks:
- Go back to ""Messages API Sandbox"".
- Update the webhook URLs:
- Inbound URL:
<your-ngrok-https-url>/webhooks/inbound
- Status URL:
<your-ngrok-https-url>/webhooks/status
- Inbound URL:
- Click ""Save webhooks"".
You have now fully configured Vonage and linked it to your local development environment via ngrok.
3. Implementing Core Functionality
Now let's write the Node.js code to send and receive messages.
Part 1: Sending an SMS (index.js
)
This script demonstrates sending a single outbound SMS message using the credentials and Vonage number configured.
// index.js
require('dotenv').config();
const { Vonage } = require('@vonage/server-sdk');
// --- Configuration ---
// Ensure these environment variables are set in your .env file
const VONAGE_API_KEY = process.env.VONAGE_API_KEY;
const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET;
const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID;
const VONAGE_PRIVATE_KEY = process.env.VONAGE_PRIVATE_KEY;
const VONAGE_SMS_FROM_NUMBER = process.env.VONAGE_SMS_FROM_NUMBER;
// --- IMPORTANT: Replace with the recipient's phone number ---
const TO_NUMBER = 'REPLACE_WITH_RECIPIENT_PHONE_NUMBER'; // e.g., '14155550100' (use E.164 format)
// ---
// --- Input Validation ---
if (!VONAGE_API_KEY || !VONAGE_API_SECRET || !VONAGE_APPLICATION_ID || !VONAGE_PRIVATE_KEY || !VONAGE_SMS_FROM_NUMBER) {
console.error('Error: Missing required Vonage credentials in .env file.');
process.exit(1); // Exit if configuration is incomplete
}
if (!TO_NUMBER || TO_NUMBER === 'REPLACE_WITH_RECIPIENT_PHONE_NUMBER') {
console.error('Error: Please replace TO_NUMBER with a valid recipient phone number in index.js');
process.exit(1);
}
// Basic format check - allows digits, optionally preceded by '+', up to 15 digits total.
// Note: This is a basic check and doesn't fully validate E.164 format structure.
if (!/^\+?\d{1,15}$/.test(TO_NUMBER)) {
console.error(`Error: Invalid TO_NUMBER format: ${TO_NUMBER}. Use E.164 format (e.g., 14155550100 or +14155550100).`);
process.exit(1);
}
// Same basic check for the sender number
if (!/^\+?\d{1,15}$/.test(VONAGE_SMS_FROM_NUMBER)) {
console.error(`Error: Invalid VONAGE_SMS_FROM_NUMBER format: ${VONAGE_SMS_FROM_NUMBER}. Use E.164 format (e.g., 12015550123 or +12015550123).`);
process.exit(1);
}
// --- Initialize Vonage Client ---
// Using Application ID and Private Key for authentication is recommended for Messages API
const vonage = new Vonage({
apiKey: VONAGE_API_KEY,
apiSecret: VONAGE_API_SECRET, // Needed for fallback or other APIs, though Messages prefers JWT
applicationId: VONAGE_APPLICATION_ID,
privateKey: VONAGE_PRIVATE_KEY,
});
// --- Send SMS Message ---
const text = 'Hello from Vonage and Node.js!'; // Your message content
async function sendSms() {
// Remove leading '+' if present for the API call, as per SDK examples for `to`/`from`
const recipientNumber = TO_NUMBER.replace('+', '');
const senderNumber = VONAGE_SMS_FROM_NUMBER.replace('+', '');
console.log(`Attempting to send SMS from ${senderNumber} to ${recipientNumber}...`);
try {
const response = await vonage.messages.send({
message_type: 'text',
text: text,
to: recipientNumber,
from: senderNumber,
channel: 'sms',
});
console.log(`SMS submitted successfully with Message UUID: ${response.messageUuid}`);
console.log('Note: Successful submission means Vonage accepted the request. Check status webhooks or message logs for delivery status.');
} catch (error) {
console.error('Error sending SMS:');
// Log detailed error information if available
if (error.response && error.response.data) {
console.error(`Status: ${error.response.status}`);
console.error(`Headers: ${JSON.stringify(error.response.headers)}`);
console.error(`Data: ${JSON.stringify(error.response.data)}`);
} else {
console.error(error);
}
}
}
// --- Execute Sending Function ---
sendSms();
Explanation (index.js
):
require('dotenv').config();
: Loads environment variables from the.env
file intoprocess.env
.- Configuration Loading & Validation: Reads the necessary Vonage credentials and numbers from
process.env
and performs basic checks to ensure they are present andTO_NUMBER
is set. It also performs a simple regex check for phone number format. Note that this regex (/^\+?\d{1,15}$/
) is a very basic check; it ensures the string contains only digits (optionally preceded by a+
) and is within the typical length limits, but it doesn't rigorously enforce the full E.164 standard (e.g., specific country code rules). The error message guides the user towards the correct format. - Vonage Client Initialization: Creates an instance of the Vonage client using the Application ID and Private Key for JWT authentication, which is the preferred method for the Messages API. API Key/Secret are included for potential fallback or use with other Vonage APIs.
vonage.messages.send({...})
: This is the core function call.message_type: 'text'
: Specifies a plain text message.text
: The content of the message.to
: The recipient's phone number (loaded from theTO_NUMBER
constant, with leading+
removed for the API call). Crucially, replace the placeholder value.from
: Your Vonage virtual number (loaded from.env
, with leading+
removed).channel: 'sms'
: Explicitly tells the Messages API to use the SMS channel.
- Async/Await & Error Handling: The
send
function returns a Promise, so we useasync/await
for cleaner handling. Atry...catch
block captures potential errors during the API call (e.g., network issues, invalid credentials, insufficient funds) and logs detailed information if available from the error response. - Execution: Calls the
sendSms()
function to trigger the process.
Part 2: Receiving Messages & Replying (WhatsApp) (server.js
)
This script sets up an Express server to listen for incoming webhooks from Vonage for both SMS and WhatsApp messages. It includes JWT verification for security and demonstrates automatically replying to incoming WhatsApp messages.
// server.js
require('dotenv').config();
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const { WhatsAppText } = require('@vonage/messages');
const { verifySignature } = require('@vonage/jwt');
// --- Configuration ---
const PORT = process.env.PORT || 8000;
const VONAGE_API_KEY = process.env.VONAGE_API_KEY;
const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET;
const VONAGE_APPLICATION_ID = process.env.VONAGE_APPLICATION_ID;
const VONAGE_PRIVATE_KEY = process.env.VONAGE_PRIVATE_KEY;
const VONAGE_API_SIGNATURE_SECRET = process.env.VONAGE_API_SIGNATURE_SECRET;
const VONAGE_WHATSAPP_NUMBER = process.env.VONAGE_WHATSAPP_NUMBER; // Sandbox number
// --- Input Validation ---
if (!VONAGE_API_KEY || !VONAGE_API_SECRET || !VONAGE_APPLICATION_ID || !VONAGE_PRIVATE_KEY || !VONAGE_API_SIGNATURE_SECRET || !VONAGE_WHATSAPP_NUMBER) {
console.error('Error: Missing required Vonage credentials or configuration in .env file for the server.');
process.exit(1);
}
// --- Initialize Express App ---
const app = express();
// Use express.json() for parsing application/json
app.use(express.json());
// Use express.urlencoded() for parsing application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
// --- Initialize Vonage Client ---
// Use Sandbox host for WhatsApp Sandbox testing
const vonage = new Vonage(
{
apiKey: VONAGE_API_KEY,
apiSecret: VONAGE_API_SECRET,
applicationId: VONAGE_APPLICATION_ID,
privateKey: VONAGE_PRIVATE_KEY,
},
{
// IMPORTANT: Use the sandbox API host for WhatsApp Sandbox messages
apiHost: 'https://messages-sandbox.nexmo.com',
}
);
// --- Security Middleware: Verify Vonage Signature ---
// This function acts as middleware to verify the JWT signature on incoming requests
const verifyVonageSignature = (req, res, next) => {
try {
// Extract token from 'Authorization: Bearer <token>' header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
console.warn('Webhook received without valid Bearer token.');
return res.status(401).send('Unauthorized: Missing Bearer token');
}
const token = authHeader.split(' ')[1];
// Verify the signature using the secret from .env
if (!verifySignature(token, VONAGE_API_SIGNATURE_SECRET)) {
console.error('Webhook received with invalid signature.');
return res.status(401).send('Unauthorized: Invalid signature');
}
console.log('Webhook signature verified successfully.');
// If signature is valid, proceed to the next middleware/route handler
next();
} catch (error) {
console.error('Error during signature verification:', error);
res.status(500).send('Internal Server Error during signature verification');
}
};
// --- Function to Send WhatsApp Reply ---
const sendWhatsAppReply = async (recipientNumber, messageText) => {
console.log(`Attempting to send WhatsApp reply to ${recipientNumber}...`);
try {
const response = await vonage.messages.send(
new WhatsAppText({
text: messageText,
to: recipientNumber, // The number that sent the inbound message
from: VONAGE_WHATSAPP_NUMBER, // Your Vonage WhatsApp Sandbox number
})
);
console.log(`WhatsApp reply submitted successfully with Message UUID: ${response.messageUuid}`);
} catch (error) {
console.error('Error sending WhatsApp reply:');
if (error.response && error.response.data) {
console.error(`Status: ${error.response.status}`);
console.error(`Headers: ${JSON.stringify(error.response.headers)}`);
console.error(`Data: ${JSON.stringify(error.response.data)}`);
} else {
console.error(error);
}
}
};
// --- Webhook Endpoints ---
// Endpoint for INBOUND messages (SMS & WhatsApp)
// Apply signature verification middleware FIRST
app.post('/webhooks/inbound', verifyVonageSignature, async (req, res) => {
console.log('--- Inbound Message Received ---');
console.log('Timestamp:', new Date().toISOString());
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { channel, from: senderNumberObj, message_type, text } = req.body;
const senderNumber = senderNumberObj?.number; // Extract number from the 'from' object
if (!senderNumber) {
console.error('Could not extract sender number from inbound webhook:', req.body);
// Still send 200 OK to prevent retries, but log the error
return res.status(200).send('OK');
}
console.log(`Channel: ${channel}, From: ${senderNumber}, Type: ${message_type}`);
// Handle based on channel
if (channel === 'whatsapp') {
console.log('Received a WhatsApp message.');
// Automatically reply to incoming WhatsApp messages
const replyText = `Hi there! You sent: "${text}". This is an automated reply from the Vonage Node.js server.`;
// Ensure the senderNumber is correctly formatted if needed (it should be E.164 from Vonage)
await sendWhatsAppReply(senderNumber, replyText);
} else if (channel === 'sms') {
console.log('Received an SMS message.');
// Add specific SMS handling logic here if needed (e.g., store in DB, trigger workflow)
// For this example, we just log it.
} else {
console.log(`Received message from unhandled channel: ${channel}`);
}
// IMPORTANT: Always respond with 200 OK to acknowledge receipt
// Otherwise Vonage will retry sending the webhook
res.status(200).send('OK');
});
// Endpoint for message STATUS updates (Delivery Receipts etc.)
// Apply signature verification middleware FIRST
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
console.log('--- Status Update Received ---');
console.log('Timestamp:', new Date().toISOString());
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// Process the status update (e.g., update message status in your database)
// Example statuses: submitted, delivered, rejected, undeliverable
// Acknowledge receipt
res.status(200).send('OK');
});
// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
res.status(200).send('Server is healthy');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`ngrok should be forwarding to http://localhost:${PORT}`);
console.log('Ensure your Vonage Application and Sandbox webhooks point to your ngrok URL + /webhooks/inbound and /webhooks/status');
});
Explanation (server.js
):
- Dependencies & Config: Loads
dotenv
,express
, Vonage SDK components, and configuration variables. Includes validation. - Express Setup: Initializes the Express app and configures middleware (
express.json()
,express.urlencoded()
) to parse incoming request bodies. - Vonage Client Initialization: Creates the Vonage client instance. Crucially, it specifies
{ apiHost: 'https://messages-sandbox.nexmo.com' }
in the options. This directs API calls (specifically thesendWhatsAppReply
) to the correct Sandbox endpoint. Important Limitation: Because thisapiHost
is hardcoded, this specificvonage
client instance cannot be used to send production WhatsApp messages (which use the standardmessages.nexmo.com
host) or potentially SMS messages directly from the server without modification. For a production application sending multiple message types from the server, you might need separate client instances or logic to determine the correctapiHost
based on the message type or environment. verifyVonageSignature
Middleware:- This function is designed to run before the main route handlers for
/webhooks/inbound
and/webhooks/status
. - It extracts the JWT token from the
Authorization: Bearer <token>
header sent by Vonage. - It uses
verifySignature(token, VONAGE_API_SIGNATURE_SECRET)
from@vonage/jwt
to check if the token's signature is valid using the secret stored in.env
. - If the signature is invalid or the header is missing/malformed, it sends a
401 Unauthorized
response and stops processing. - If valid, it calls
next()
to pass control to the actual webhook handler (app.post(...)
). This is a critical security step to ensure webhooks genuinely originate from Vonage.
- This function is designed to run before the main route handlers for
sendWhatsAppReply
Function:- An
async
function dedicated to sending replies via WhatsApp. - Uses
vonage.messages.send()
but wraps the message details innew WhatsAppText({...})
provided by@vonage/messages
for convenience. - Sends the reply
from
the Sandbox numberto
the original sender (recipientNumber
). - Includes error handling specific to sending the reply.
- An
/webhooks/inbound
Endpoint:- Handles
POST
requests to this path (where Vonage sends incoming messages). - Crucially applies the
verifyVonageSignature
middleware first. - Logs the entire request body for debugging.
- Extracts key information:
channel
,from
(sender number is extracted from thefrom
object, e.g.,req.body.from.number
),message_type
,text
. Includes a basic check to ensure the sender number was extracted. - Includes logic to differentiate between
whatsapp
andsms
channels. - If it's a WhatsApp message, it calls
sendWhatsAppReply
to send an automated response. - If it's SMS, it just logs it (you would add custom logic here).
- Responds with
res.status(200).send('OK');
. This is vital. Vonage expects a 2xx response to confirm receipt; otherwise, it will retry sending the webhook, potentially causing duplicate processing.
- Handles
/webhooks/status
Endpoint:- Handles
POST
requests for status updates (e.g., delivery confirmations). - Also applies the
verifyVonageSignature
middleware. - Logs the status update body. In a real application, you'd parse this to update the status of sent messages in your database.
- Responds with
200 OK
.
- Handles
/health
Endpoint: A simpleGET
endpoint useful for basic monitoring or load balancers to check if the server is running.app.listen
: Starts the Express server, making it listen for connections on the configuredPORT
.
4. Verification and Testing
Let's test both sending and receiving capabilities.
Ensure ngrok
is running and your Vonage webhook URLs are correctly updated.
Test 1: Sending an Outbound SMS
-
Edit
index.js
: Openindex.js
and replace'REPLACE_WITH_RECIPIENT_PHONE_NUMBER'
with your actual mobile phone number in E.164 format (e.g.,14155550100
or+14155550100
). Save the file. -
Run the Script: In your terminal (the one not running ngrok), execute:
node index.js
-
Check Output: You should see console output indicating the SMS was submitted successfully, including a
Message UUID
. -
Check Your Phone: You should receive the SMS message (""Hello from Vonage and Node.js!"") on the phone number you specified. Delivery might take a few seconds.
Test 2: Receiving Messages and WhatsApp Reply
-
Start the Server: In your terminal (the one not running ngrok, if you stopped it after sending SMS), start the Express server:
node server.js
-
Check Server Logs: You should see ""Server listening on port 8000..."".
-
Receive an SMS:
- From your mobile phone, send an SMS message to the Vonage virtual number you linked to your application (
VONAGE_SMS_FROM_NUMBER
in.env
). - Observe Server Logs: You should see the ""--- Inbound Message Received ---"" log, detailing the SMS message (
channel: 'sms'
), your phone number, and the message text. The signature verification log should also appear. - Observe ngrok Console: You should see
POST /webhooks/inbound 200 OK
requests in the ngrok terminal window.
- From your mobile phone, send an SMS message to the Vonage virtual number you linked to your application (
-
Receive a WhatsApp Message & Trigger Reply:
- Ensure you have already joined the Vonage Sandbox from your WhatsApp number (Step 4 in Vonage Configuration).
- From your WhatsApp-enabled phone, send any message to the Vonage Sandbox WhatsApp number (
+14157386102
). - Observe Server Logs:
- You'll see the ""--- Inbound Message Received ---"" log (
channel: 'whatsapp'
). - You'll see the ""Attempting to send WhatsApp reply..."" log.
- You'll see the ""WhatsApp reply submitted successfully..."" log.
- You'll see the ""--- Inbound Message Received ---"" log (
- Check Your Phone (WhatsApp): You should receive an automated reply from the Sandbox number like: ""Hi there! You sent: ""[Your Message]"". This is an automated reply..."".
- Observe ngrok Console: You should see
POST /webhooks/inbound 200 OK
requests.
-
Check Status Webhooks (Optional but Recommended):
- After sending the SMS (
index.js
) and the WhatsApp reply (server.js
), you should eventually see ""--- Status Update Received ---"" logs in yourserver.js
console as Vonage sends delivery confirmations (or failures) to the/webhooks/status
endpoint. This might take a few seconds to minutes depending on the network.
- After sending the SMS (
5. Error Handling and Logging
Our current implementation includes basic error handling and logging:
- API Call Errors (
index.js
,server.js
):try...catch
blocks wrapvonage.messages.send
. Errors are logged to the console, including details from the error response if available (error.response.data
). - Configuration Errors (
index.js
,server.js
): Basic checks for missing environment variables prevent the scripts from running without necessary config, exiting gracefully (process.exit(1)
). - Webhook Signature Errors (
server.js
): TheverifyVonageSignature
middleware explicitly handles invalid or missing signatures, logging warnings/errors and returning401 Unauthorized
. - Logging (
server.js
):console.log
is used to track incoming messages, status updates, signature verification results, and reply attempts. Timestamps are added for context. - Webhook Acknowledgement (
server.js
): Sendingres.status(200).send('OK')
prevents Vonage's retry mechanism from flooding the server if processing takes time or fails silently.
Production Considerations:
While the current setup is functional for demonstration, a production environment requires more robustness:
- Structured Logging: Use a dedicated logging library (e.g., Winston, Pino) for structured logging (JSON format), different log levels (info, warn, error), and configurable outputs (console, file, external services).
- Centralized Logging: Ship logs to a centralized logging platform (e.g., Datadog, ELK stack, Logtail) for easier searching, analysis, and alerting.
- Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) to capture, aggregate, and alert on unhandled exceptions.
- More Granular Error Handling: Implement more specific error handling based on Vonage API error codes or types (e.g., differentiate between invalid number format, insufficient funds, throttling).
- Retry Mechanisms (for Sending): For critical outbound messages, implement a retry strategy (e.g., using libraries like
async-retry
) with exponential backoff if the initialvonage.messages.send
call fails due to temporary network issues or API unavailability. Be cautious not to retry errors that are clearly permanent (like invalid numbers).
6. Security Features
Security is paramount, especially when handling communication and API keys.
- Environment Variables (
.env
): Keeping API keys, secrets, and application IDs out of source code (and using.gitignore
to exclude.env
) is the most fundamental security practice. - Webhook Signature Verification (
server.js
): TheverifyVonageSignature
middleware is crucial. It ensures that incoming requests to your webhook endpoints genuinely originate from Vonage and haven't been tampered with or spoofed. Never disable this in production. - HTTPS for Webhooks: Using
ngrok
provides an HTTPS URL during development. In production, ensure your webhook endpoint is served over HTTPS (using a reverse proxy like Nginx or Caddy with SSL certificates, or platform-provided HTTPS like Heroku, Vercel, AWS Load Balancer). This encrypts the data in transit between Vonage and your server. - Input Validation: While basic checks are included (e.g., for
TO_NUMBER
format), robust applications should validate all inputs more thoroughly, especially data coming from external sources (like webhook payloads or user input used in replies) to prevent injection attacks or unexpected behavior. - Rate Limiting: Consider implementing rate limiting on your webhook endpoints (using middleware like
express-rate-limit
) to prevent abuse or denial-of-service attacks. - Least Privilege: Ensure the Vonage API key/secret or Application ID/Private Key used have only the necessary permissions required for the application's functionality.