Developer Guide: Sending and Receiving SMS & WhatsApp Messages with Node.js, Express, and Vonage
A step-by-step guide to building a Node.js and Express application for sending and receiving SMS and WhatsApp messages using the Vonage Messages API and SDK.
This guide provides a step-by-step walkthrough for building a production-ready Node.js application using the Express framework to send and receive both SMS and WhatsApp messages via the Vonage Messages API. This guide utilizes the Express framework to structure the application and the Vonage Node SDK to handle communication with the Vonage APIs. We will cover project setup, core functionality, webhook handling, security considerations, testing, and deployment practices.
Goal: To create a unified backend service capable of handling bidirectional communication over SMS and WhatsApp, suitable for applications like customer support bots, notification systems, or interactive campaigns.
Technologies Used:
Node.js: A JavaScript runtime environment for server-side development. (Version 18 or higher recommended).
Express: A minimal and flexible Node.js web application framework.
Vonage Messages API: A powerful API for sending and receiving messages across multiple channels (SMS, MMS, WhatsApp, Facebook Messenger, Viber).
Vonage Node SDK: Simplifies interaction with Vonage APIs in Node.js applications.
ngrok: A tool to expose local servers to the internet for webhook testing during development.
dotenv: A module to load environment variables from a .env file.
System Architecture:
(Diagram placeholder: Illustrates message flow: User <-> Vonage <-> ngrok (dev) / Public URL (prod) <-> Your Node.js App)
Prerequisites:
Node.js and npm: Installed on your system (Version 18 or higher recommended). While Node.js v18+ is recommended, ensure compatibility within your specific target Node.js version. Download Node.js
Vonage API Account: Sign up for free credit. Vonage Signup
Vonage CLI (Optional but Recommended): Install via npm: npm install -g @vonage/cli. Configure it with vonage config setup API_KEY API_SECRET.
ngrok Account & Installation: A free account is sufficient for testing. Download ngrok. Note: For stable webhook URLs in development or production alternatives, consider paid ngrok plans, services like Cloudflare Tunnel, or deploying to a publicly accessible server.
A Vonage Virtual Number: Purchase one from the Vonage Dashboard (Numbers -> Buy numbers) capable of sending/receiving SMS in your desired region.
WhatsApp Enabled Device: For testing WhatsApp functionality.
1. Project Setup and Configuration
Let's initialize our Node.js project and install the necessary dependencies.
1.1 Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it.
Bash
mkdir vonage-unified-messaging
cd vonage-unified-messaging
1.2 Initialize npm:
Initialize the project using npm. The -y flag accepts default settings.
Bash
npm init -y
This creates a package.json file.
1.3 Install Dependencies:
Install Express for the web server, the Vonage SDKs for API interaction, and dotenv for environment variable management.
Open the .env file and add the following placeholders. We will populate these values in the next steps.
.env
# Vonage API Credentials
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY=./private.key # Path to your downloaded private key file
VONAGE_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET # Used for webhook verification
# Vonage Numbers
VONAGE_SMS_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Used as 'from' for SMS (e.g., 15551112222)
VONAGE_WHATSAPP_NUMBER=YOUR_VONAGE_WHATSAPP_SANDBOX_NUMBER # e.g., 14157386102 for Sandbox
# Server Configuration
PORT=3000 # Port the Express server will run on
Explanation of .env variables:
VONAGE_API_KEY, VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.
VONAGE_APPLICATION_ID: Generated when you create a Vonage Application (see next section).
VONAGE_PRIVATE_KEY: Path to the private.key file downloaded when creating the Vonage Application. Place this file in your project root or update the path accordingly. See Security section for production handling.
VONAGE_SIGNATURE_SECRET: Found in your Vonage Dashboard Settings. Used to verify webhook authenticity.
VONAGE_SMS_NUMBER: Your purchased Vonage virtual number (include country code, no '+', e.g., 15551112222).
VONAGE_WHATSAPP_NUMBER: The number assigned by the Vonage WhatsApp Sandbox (e.g., 14157386102).
PORT: The local port your server will listen on.
2. Vonage Application and Sandbox Setup
We need a Vonage Application to handle message routing and authentication, and the WhatsApp Sandbox for testing.
2.1 Create a Vonage Application:
A Vonage Application acts as a container for your communication settings and authentication.
Click Generate public and private key. Immediately save the private.key file that downloads. Place it in your project root directory (or update the .env path). Vonage does not store this private key, so keep it safe.
Enable the Messages capability.
You'll need webhook URLs. We'll use ngrok for this temporarily. Open a new terminal window, navigate to your project directory, and start ngrok:
Bash
# Make sure ngrok is installed and configured
ngrok http 3000 # Match the PORT in your .env file
Ngrok will display a public Forwarding URL (e.g., https://<unique-code>.ngrok-free.app). Copy the https URL. Keep ngrok running.
Back in the Vonage Application setup, paste the ngrok URL into the Inbound URL and Status URL fields, appending the paths we'll create in our Express app:
Inbound URL:YOUR_NGROK_HTTPS_URL/webhooks/inbound
Status URL:YOUR_NGROK_HTTPS_URL/webhooks/status(Example: https://abcdef123456.ngrok-free.app/webhooks/inbound)
Click Generate new application.
Copy the Application ID displayed and paste it into the VONAGE_APPLICATION_ID field in your .env file.
Link Your Number: Scroll down to Link virtual numbers and link the Vonage virtual number you purchased for SMS. This routes incoming SMS for that number to this application's inbound webhook.
2.2 Set Default SMS API (Important):
Ensure your account uses the Messages API (not the older SMS API) for webhooks.
The Sandbox provides a testing environment without needing a dedicated WhatsApp Business number initially.
Navigate to Messages Sandbox under Developer Tools in the Dashboard menu.
Activate the WhatsApp Sandbox if you haven't already.
Scan the QR code with your WhatsApp app or send the specified message to the Sandbox number from your personal WhatsApp account to ""allowlist"" your number for testing.
Copy the Vonage Sandbox Number (e.g., 14157386102) and paste it into VONAGE_WHATSAPP_NUMBER in your .env file.
Under Webhooks on the Sandbox page:
Set Inbound Message URL:YOUR_NGROK_HTTPS_URL/webhooks/inbound (Same as application)
Set Status URL:YOUR_NGROK_HTTPS_URL/webhooks/status (Same as application)
Paste this value into VONAGE_SIGNATURE_SECRET in your .env file.
Your .env file should now be populated with real values.
3. Implementing the Express Server and Core Logic
Now, let's write the Node.js code.
index.js:
JavaScript
// index.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const { WhatsAppText } = require('@vonage/messages');
const { verifySignature } = require('@vonage/jwt');
// --- Initialization ---const app = express();
app.use(express.json()); // Middleware to parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded bodiesconstPORT = process.env.PORT || 3000;
// Initialize Vonage Client. Authentication for sending Messages API calls primarily uses// Application ID and Private Key to generate JWTs. API Key/Secret may be used for// other SDK functions or legacy APIs. The Signature Secret is used for webhook verification.const vonage = newVonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY,
});
// --- Security Middleware: Verify Vonage Signature ---// Protects webhooks from unauthorized accessconstverifyVonageSignature = (req, res, next) => {
try {
// Extract token from 'Authorization: Bearer <token>' headerconst token = req.headers.authorization?.split(' ')[1];
if (!token) {
console.warn('[WARN] Webhook Error: Missing Authorization header.');
return res.status(401).send('Unauthorized: Missing token');
}
// Verify using the secret from .envconst isValid = verifySignature(token, process.env.VONAGE_SIGNATURE_SECRET);
if (isValid) {
console.log('[INFO] Webhook Signature Verified Successfully.');
next(); // Proceed to the route handler
} else {
console.warn('[WARN] Webhook Error: Invalid Signature.');
res.status(401).send('Unauthorized: Invalid signature');
}
} catch (error) {
console.error('[ERROR] Webhook Error: Signature verification failed:', error);
res.status(500).send('Internal Server Error');
}
};
// --- Webhook Endpoints ---// Handles incoming messages (SMS & WhatsApp)
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
console.log('[INFO] ------------------------------------');
console.log('[INFO] Incoming Message Webhook Received:');
console.log('[DEBUG] Timestamp:', newDate().toISOString());
console.log('[DEBUG] Body:', JSON.stringify(req.body, null, 2)); // Log the full payloadconst { from, to, channel, message_type, text, image } = req.body;
// Basic validationif (!from || !channel || !message_type) {
console.warn('[WARN] Inbound Webhook: Received incomplete data.');
return res.status(400).send('Bad Request: Missing required fields.');
}
console.log(`[INFO] Message received on ${channel} from ${from.number || from.id} to ${to.number || to.id}`);
// --- Channel-Specific Logic ---if (channel === 'sms') {
console.log(`[INFO] SMS Received: "${text}"`);
// TODO: Add your custom business logic here (e.g., database interaction, analytics, specific replies).// Example: Store message, trigger automated reply, etc.
} elseif (channel === 'whatsapp') {
if (message_type === 'text') {
console.log(`[INFO] WhatsApp Text Received: "${text}"`);
// TODO: Add your custom business logic for WhatsApp text here.
} elseif (message_type === 'image') {
console.log(`[INFO] WhatsApp Image Received: URL=${image.url}`);
// TODO: Add your custom business logic for WhatsApp images here.// Note: Image URLs might be temporary. Download if needed promptly.
} else {
console.log(`[INFO] WhatsApp Message Type Received: ${message_type}`);
// TODO: Handle other WhatsApp types (audio, video, file, location, etc.) as needed.
}
// Example: Send a confirmation reply back via WhatsAppsendWhatsAppConfirmation(from.id); // Use from.id for WhatsApp replies
} else {
console.log(`[WARN] Received message on unhandled channel: ${channel}`);
}
// Vonage expects a 200 OK response to acknowledge receipt
res.status(200).send('OK');
console.log('[INFO] ------------------------------------');
});
// Handles message status updates (e.g., delivered, read)
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
console.log('[INFO] ------------------------------------');
console.log('[INFO] Status Webhook Received:');
console.log('[DEBUG] Timestamp:', newDate().toISOString());
console.log('[DEBUG] Body:', JSON.stringify(req.body, null, 2)); // Log the full payloadconst { message_uuid, status, timestamp, channel, to, from, error } = req.body;
console.log(`[INFO] Status update for message ${message_uuid} (${channel}): ${status}`);
if (error) {
console.error(`[ERROR] Message Error for ${message_uuid}: Type=${error.type}, Reason=${error.reason}`);
}
// TODO: Add logic here to update message status in your database or tracking system.
res.status(200).send('OK'); // Acknowledge receiptconsole.log('[INFO] ------------------------------------');
});
// --- API Endpoint for Sending Messages ---// Example: POST /send-message with JSON body:// { "channel": "sms", "to": "15551234567", "text": "Hello via SMS!" }// { "channel": "whatsapp", "to": "15551234567", "text": "Hello via WhatsApp!" }
app.post('/send-message', async (req, res) => {
console.log('[INFO] ------------------------------------');
console.log('[INFO] Send Message API Request Received:');
const { channel, to, text } = req.body;
// Basic Input Validationif (!channel || !to || !text) {
console.error('[ERROR] Send API Error: Missing channel, to, or text in request body.');
return res.status(400).json({ error: 'Missing required fields: channel, to, text' });
}
if (!['sms', 'whatsapp'].includes(channel)) {
console.error(`[ERROR] Send API Error: Invalid channel specified: ${channel}`);
return res.status(400).json({ error: 'Invalid channel. Must be "sms" or "whatsapp".' });
}
// Determine the 'from' number based on the channelconst fromNumber = channel === 'sms'
? process.env.VONAGE_SMS_NUMBER
: process.env.VONAGE_WHATSAPP_NUMBER;
if (!fromNumber) {
console.error(`[ERROR] Send API Error: Missing Vonage number for channel ${channel} in .env`);
return res.status(500).json({ error: `Configuration error: Vonage number for ${channel} not set.` });
}
console.log(`[INFO] Attempting to send ${channel} message to ${to} from ${fromNumber}`);
try {
let response;
if (channel === 'sms') {
response = await vonage.messages.send({
message_type: "text",
text: text,
to: to,
from: fromNumber,
channel: "sms"
});
} elseif (channel === 'whatsapp') {
// For WhatsApp, use the specific WhatsAppText class
response = await vonage.messages.send(
newWhatsAppText({
text: text,
to: to, // Recipient's WhatsApp number (allowlisted in Sandbox)from: fromNumber, // Your Vonage WhatsApp Sandbox number
})
);
}
console.log(`[INFO] Message sent successfully via ${channel}!`);
console.log('[DEBUG] API Response:', response); // Includes message_uuid
res.status(200).json({ success: true, message_uuid: response.message_uuid });
} catch (error) {
console.error(`[ERROR] Error sending ${channel} message to ${to}:`, error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
// Provide more detail if available from Vonage error responseconst errorDetails = error.response?.data || { message: error.message };
res.status(error.response?.status || 500).json({
success: false,
error: `Failed to send ${channel} message.`,
details: errorDetails
});
}
console.log('[INFO] ------------------------------------');
});
// --- Helper Function (Example for WhatsApp Reply) ---asyncfunctionsendWhatsAppConfirmation(recipientNumber) {
console.log(`[INFO] Sending WhatsApp confirmation to ${recipientNumber}...`);
try {
const response = await vonage.messages.send(
newWhatsAppText({
text: "*Message received!*", // Example using Markdown for italics in WhatsAppto: recipientNumber,
from: process.env.VONAGE_WHATSAPP_NUMBER,
})
);
console.log(`[INFO] WhatsApp confirmation sent: ${response.message_uuid}`);
} catch (error) {
console.error(`[ERROR] Error sending WhatsApp confirmation to ${recipientNumber}:`, error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
}
}
// --- Start Server ---
app.listen(PORT, () => {
console.log(`[INFO] Server listening on port ${PORT}`);
console.log(`[INFO] Ensure your ngrok tunnel's HTTPS URL is configured for Vonage webhooks.`);
console.log(`[INFO] Webhooks in Vonage Dashboard/Sandbox must point to YOUR_NGROK_HTTPS_URL/webhooks/...`);
});
// --- Basic Error Handling (Catch-all) ---
app.use((err, req, res, next) => {
console.error("[ERROR] Unhandled Error:", err.stack || err);
res.status(500).send('Something broke!');
});
Code Explanation:
Initialization: Loads .env, requires modules, initializes Express and the Vonage SDK.
Middleware: Uses express.json() and express.urlencoded() to parse incoming request bodies.
verifyVonageSignature Middleware: This is crucial for webhook security. It extracts the JWT from the Authorization: Bearer <token> header sent by Vonage, verifies it against your VONAGE_SIGNATURE_SECRET using @vonage/jwt. If invalid, it rejects the request with a 401 Unauthorized status.
/webhooks/inbound Route:
Protected by verifyVonageSignature.
Logs the incoming request body (using structured logging prefixes).
Parses key fields like from, to, channel, message_type, text.
Includes basic logic to differentiate between sms and whatsapp channels.
Includes placeholders (// TODO:) explicitly marked for implementing your specific business logic (e.g., database storage, triggering workflows).
Includes an example call to sendWhatsAppConfirmation upon receiving a WhatsApp message.
Crucially, it sends a 200 OK response. Vonage needs this acknowledgment; otherwise, it will retry sending the webhook, potentially causing duplicate processing.
/webhooks/status Route:
Protected by verifyVonageSignature.
Logs status updates (e.g., delivered, read, failed).
Includes placeholders (// TODO:) for updating message status in your system.
Sends a 200 OK response.
/send-message API Route:
An example internal API endpoint you can call to send outbound messages.
Takes channel, to, and text in the JSON request body.
Performs basic validation.
Selects the correct from number based on the channel.
Uses vonage.messages.send():
For SMS, it sends a simple object.
For WhatsApp, it uses the WhatsAppText class helper from @vonage/messages.
Includes try...catch for error handling during the API call, logging detailed errors.
Returns a JSON response indicating success or failure, including the message_uuid on success or error details on failure.
sendWhatsAppConfirmation Helper: A simple example function demonstrating how to send a reply back to a WhatsApp user. Note the use of Markdown (*...*) within the text string for formatting in WhatsApp.
Server Start: Listens on the configured PORT, provides informative startup logs.
Basic Error Handler: A catch-all middleware for unhandled errors.
4. Running and Testing the Application
4.1 Start the Server:
Ensure your ngrok tunnel (from step 2.1.6) is still running in one terminal. In your main project terminal, run:
Bash
node index.js
You should see output similar to:
console
[INFO] Server listening on port 3000
[INFO] Ensure your ngrok tunnel's HTTPS URL is configured for Vonage webhooks.
[INFO] Webhooks in Vonage Dashboard/Sandbox must point to YOUR_NGROK_HTTPS_URL/webhooks/...
(Replace YOUR_NGROK_HTTPS_URL in the Vonage settings with the actual URL provided by ngrok).
4.2 Testing Inbound SMS:
Send an SMS from your physical phone to your purchased Vonage Virtual Number (VONAGE_SMS_NUMBER).
Check the terminal running node index.js. You should see logs for:
[INFO] WhatsApp Text Received: ""Your message text""
[INFO] Sending WhatsApp confirmation to <your_whatsapp_number>...
[INFO] WhatsApp confirmation sent: <message_uuid>
Check your personal WhatsApp – you should receive the ""Message received!"" reply from the Sandbox number.
4.4 Testing Outbound API (/send-message):
Use curl or a tool like Postman/Insomnia to send requests to your local server's API endpoint (http://localhost:3000/send-message).
Send SMS:
Bash
curl -X POST http://localhost:3000/send-message \
-H ""Content-Type: application/json"" \
-d '{
""channel"": ""sms"",
""to"": ""YOUR_PERSONAL_PHONE_NUMBER"",
""text"": ""Hello from Node.js via Vonage SMS API!""
}'
(Replace YOUR_PERSONAL_PHONE_NUMBER with your actual number, including country code, e.g., 15551234567)
Check your terminal for logs indicating the attempt and success/failure.
Check your phone for the incoming SMS.
Send WhatsApp:
Bash
curl -X POST http://localhost:3000/send-message \
-H ""Content-Type: application/json"" \
-d '{
""channel"": ""whatsapp"",
""to"": ""YOUR_ALLOWLISTED_WHATSAPP_NUMBER"",
""text"": ""Hello from Node.js via Vonage WhatsApp Sandbox!""
}'
(Replace YOUR_ALLOWLISTED_WHATSAPP_NUMBER with your number allowlisted in the Sandbox, including country code)
Check your terminal for logs.
Check your WhatsApp for the incoming message from the Sandbox number.
4.5 Testing Status Webhook:
After successfully sending messages via the API, you should eventually see logs for [INFO] Status Webhook Received: in your terminal as Vonage sends updates (e.g., submitted, delivered, read - 'read' status depends on recipient settings and channel).
4.6 Testing Error Conditions (Recommended):
Invalid Recipient: Try sending an SMS/WhatsApp message to a known invalid number format via the /send-message endpoint. Observe the error logged in the console and the API response.
Signature Failure: Use curl to send a POST request directly to your /webhooks/inbound ngrok URL without a valid Authorization: Bearer <token> header, or with an invalid token. Verify your server responds with a 401 Unauthorized and logs a signature warning.
Insufficient Funds (Simulated): While hard to force in the sandbox, be aware of potential 402 Payment Required errors from the Vonage API if your account runs out of credit. Ensure your error handling logs these appropriately.
5. Error Handling and Logging
Current Implementation: Uses basic console.log, console.warn, console.error with text prefixes ([INFO], [WARN], [ERROR], [DEBUG]). try...catch blocks wrap Vonage API calls and webhook processing logic. The JWT verification middleware handles signature errors. A basic Express error handler catches unhandled exceptions.
Production Enhancements:
Structured Logging: Use a library like Winston or Pino for structured JSON logs, which are easier to parse and analyze in log management systems (e.g., Datadog, Splunk, ELK stack). Include timestamps, log levels, request IDs (using middleware like express-request-id), and relevant context (like message_uuid).
Centralized Logging: Configure your logger to forward logs to a centralized platform for monitoring and alerting.
More Specific Error Handling: Catch specific Vonage API errors based on error.response.data.type or error.response.status (e.g., invalid number format 400, insufficient funds 402, authentication issues 401) and provide more informative responses or implement specific retry logic where appropriate.
Alerting: Set up alerts in your monitoring system for critical errors (e.g., high rate of failed messages, webhook signature failures 401, server crashes 500, authentication errors 401 on sending).
6. Security Considerations
Webhook Signature Verification:Implemented and crucial. This prevents attackers from sending fake webhook events to your server. Always keep your VONAGE_SIGNATURE_SECRET secure and treat it like a password. Rotate it if compromised.
Environment Variables:Implemented. Never hardcode API keys, secrets, private keys, or phone numbers in your source code. Use .env for local development and secure environment variable management in production (e.g., platform secrets, HashiCorp Vault, AWS Secrets Manager, Google Secret Manager). See Section 9 for private key handling.
Input Validation:
Webhooks: The current code does basic checks for essential fields. Add more robust validation (e.g., checking number formats, expected payload structure) if needed based on your business logic. Malformed webhooks should be rejected gracefully (e.g., 400 Bad Request).
/send-message API: Basic validation is included. Enhance it based on expected inputs (e.g., max text length, valid number formats using libraries like libphonenumber-js). Use libraries like Joi or express-validator for defining and enforcing complex validation schemas.
Rate Limiting: Protect your /send-message API endpoint (and potentially webhooks, though less common) from abuse or accidental loops. Use middleware like express-rate-limit.
HTTPS: ngrok provides HTTPS for testing. In production, always ensure your application is deployed behind a load balancer or reverse proxy (like Nginx, Caddy, or cloud provider services) that terminates SSL/TLS, enforcing HTTPS for all traffic, including webhooks.
Dependency Security: Regularly audit your npm dependencies for known vulnerabilities using npm audit or tools like Snyk, and update them promptly. Implement npm audit fix into your development workflow.
Private Key Handling: The VONAGE_PRIVATE_KEY path points to a file locally. Do not commit the private.key file to version control. For production, avoid storing the key as a file on the server filesystem if possible. Best practices include:
Storing the content of the private.key file in a secure environment variable (potentially base64 encoded for easier handling in some systems). Read the key content from this variable in index.js instead of a file path.
Using a dedicated secret management service (like AWS Secrets Manager, Google Secret Manager, HashiCorp Vault) to store the key content securely and fetch it at runtime.
7. Database Integration (Conceptual)
This guide focuses on the core messaging logic. For persistent storage:
Schema: Design database tables (e.g., SQL or NoSQL collections) for messages (storing message_uuid, vonage_message_uuid, channel, direction ['inbound'/'outbound'], sender_id, recipient_id, content [text/URL], status, error_info, timestamp_received, timestamp_sent, timestamp_status_update) and potentially conversations or users.
ORM/Client: Use libraries like Prisma, Sequelize (SQL), or Mongoose (MongoDB) to interact with your chosen database.
Integration Points:
In /webhooks/inbound: After validation, create a new record in your messages table with the incoming message details.
In /webhooks/status: Find the message record using the message_uuid and update its status, timestamp_status_update, and potentially error_info.
In /send-message: Before sending the message via Vonage API, create a record in your messages table (with initial status like 'pending' or 'submitted'). After a successful API call, update the record with the vonage_message_uuid returned by Vonage. If the API call fails immediately, log the error in the record.
8. Troubleshooting and Caveats
ngrok URL Changes: Free ngrok URLs change each time you restart it. You must update the webhook URLs in both the Vonage Application and the WhatsApp Sandbox settings every time ngrok restarts. Consider paid ngrok, Cloudflare Tunnel, or localtunnel alternatives for stable development URLs.
Incorrect Credentials/Secrets: Double-check VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY (content or path), and VONAGE_SIGNATURE_SECRET in your environment. Ensure the private.key file exists and is readable if using a file path, or that the environment variable contains the correct key content.
Verify VONAGE_SIGNATURE_SECRET in your environment matches the one in your Vonage Dashboard settings exactly. Copy/paste carefully.
Ensure the request actually came from Vonage (check req.headers in logs if needed).
Ensure the system clocks on your server and Vonage's servers are reasonably synchronized (JWTs often have time-based validity).
Webhook Not Receiving Events:
Verify your ngrok tunnel is running and accessible from the internet.
Verify the Inbound URL and Status URL in your Vonage Application settings exactly match your current ngrok HTTPS URL plus the correct path (/webhooks/inbound or /webhooks/status).
Verify the same URLs are correctly set in the Vonage WhatsApp Sandbox webhook settings.
Check the Vonage Dashboard for any reported webhook delivery failures (often under Application settings or logs).
Ensure your server is running (node index.js) and hasn't crashed. Check console logs for errors.
Check firewalls or network configurations if deploying outside of local ngrok setup.
Frequently asked questions
Use the Vonage Messages API and Node.js SDK. After setting up a Vonage application and configuring webhooks, you can send SMS messages by making a POST request to the /send-message endpoint with the recipient's number and the message text in the request body. The Vonage SDK simplifies the process of interacting with the API.
The Vonage Messages API is a versatile API that enables sending and receiving messages across various channels like SMS, MMS, WhatsApp, Facebook Messenger, and Viber. This API is used in conjunction with the Vonage Node.js SDK to build applications capable of rich, multi-channel communication.
Vonage uses JWT (JSON Web Tokens) for webhook security to ensure that incoming webhook requests originate from Vonage and haven't been tampered with. The verifyVonageSignature middleware in the example code verifies the JWT against your signature secret, protecting your application from unauthorized access.
ngrok is useful for testing webhooks during local development. It provides a publicly accessible URL that forwards requests to your local server. However, for production, use a stable, publicly accessible URL and deploy your application to a server.
Yes, the provided Node.js application and Vonage setup supports sending and receiving WhatsApp messages via the Vonage WhatsApp Sandbox. Ensure your WhatsApp number is allowlisted in the sandbox for testing. The code uses the WhatsAppText class from the Vonage Messages API to send WhatsApp messages.
Configure the inbound webhook URL in your Vonage application settings to point to your application's /webhooks/inbound endpoint. The Vonage server will send an HTTP POST request to this URL with message details whenever an SMS is received on your Vonage virtual number.
Vonage sends message status updates (e.g., delivered, read) to the Status URL you configured in the application settings. Implement the /webhooks/status route in your Express application to receive these updates and process them accordingly, such as storing them in a database.
A Vonage application acts as a container for your communication settings and authentication. It manages API keys, webhooks, and other configuration related to your Vonage services. It is essential for routing messages and handling security.
Use the @vonage/jwt library, specifically the verifySignature function. Implement middleware that extracts the JWT from the Authorization header and verifies it against your signature secret. Reject requests with invalid signatures.
Node.js version 18 or higher is recommended for Vonage integration. Ensure compatibility within your specific target Node.js version while keeping up-to-date with Node.js releases and security patches.
Navigate to the Messages Sandbox under Developer Tools in your Vonage dashboard. Activate the sandbox and allowlist your WhatsApp number by scanning the QR code or sending the provided message to the sandbox number. Configure the inbound and status webhooks within the sandbox settings.
Several reasons could prevent webhooks from working: ngrok tunnel may not be running or correctly configured, the webhook URL in the Vonage settings may be incorrect, credentials may be wrong, server may not be running or experiencing errors, or the private key may not exist.
Your Vonage API key and secret are found on the main page of your Vonage API dashboard. You should store these securely in environment variables, especially in a production setting.
Never commit your Vonage private.key file to version control. For production, store its contents securely, ideally in a dedicated secrets management service or as a secure environment variable. Avoid storing it directly on the server file system.
This guide provides a step-by-step walkthrough for building a production-ready Node.js application using the Express framework to send and receive both SMS and WhatsApp messages via the Vonage Messages API. This guide utilizes the Express framework to structure the application and the Vonage Node SDK to handle communication with the Vonage APIs. We will cover project setup, core functionality, webhook handling, security considerations, testing, and deployment practices.
Goal: To create a unified backend service capable of handling bidirectional communication over SMS and WhatsApp, suitable for applications like customer support bots, notification systems, or interactive campaigns.
Technologies Used:
.envfile.System Architecture:
(Diagram placeholder: Illustrates message flow: User <-> Vonage <-> ngrok (dev) / Public URL (prod) <-> Your Node.js App)
Prerequisites:
npm install -g @vonage/cli. Configure it withvonage config setup API_KEY API_SECRET.Numbers->Buy numbers) capable of sending/receiving SMS in your desired region.1. Project Setup and Configuration
Let's initialize our Node.js project and install the necessary dependencies.
1.1 Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-unified-messaging cd vonage-unified-messaging1.2 Initialize npm:
Initialize the project using npm. The
-yflag accepts default settings.This creates a
package.jsonfile.1.3 Install Dependencies:
Install Express for the web server, the Vonage SDKs for API interaction, and
dotenvfor environment variable management.express: Web framework.@vonage/server-sdk: Core Vonage SDK.@vonage/messages: Specific helpers for the Messages API (likeWhatsAppText).@vonage/jwt: For verifying webhook signatures (essential for security).dotenv: Loads environment variables from.envintoprocess.env.1.4 Create Project Structure:
Create the basic files and folders.
touch index.js .env .gitignoreindex.js: Main application file..env: Stores sensitive credentials and configuration (API keys, phone numbers). Never commit this file to version control..gitignore: Specifies files/folders Git should ignore.1.5 Configure
.gitignore:Add
node_modulesand.envto your.gitignorefile to prevent committing them.1.6 Environment Variable Setup (
.env):Open the
.envfile and add the following placeholders. We will populate these values in the next steps.Explanation of
.envvariables:VONAGE_API_KEY,VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Generated when you create a Vonage Application (see next section).VONAGE_PRIVATE_KEY: Path to theprivate.keyfile downloaded when creating the Vonage Application. Place this file in your project root or update the path accordingly. See Security section for production handling.VONAGE_SIGNATURE_SECRET: Found in your Vonage Dashboard Settings. Used to verify webhook authenticity.VONAGE_SMS_NUMBER: Your purchased Vonage virtual number (include country code, no '+', e.g.,15551112222).VONAGE_WHATSAPP_NUMBER: The number assigned by the Vonage WhatsApp Sandbox (e.g.,14157386102).PORT: The local port your server will listen on.2. Vonage Application and Sandbox Setup
We need a Vonage Application to handle message routing and authentication, and the WhatsApp Sandbox for testing.
2.1 Create a Vonage Application:
A Vonage Application acts as a container for your communication settings and authentication.
Applicationsin your Vonage API Dashboard.Create a new application.Generate public and private key. Immediately save theprivate.keyfile that downloads. Place it in your project root directory (or update the.envpath). Vonage does not store this private key, so keep it safe.Messagescapability.# Make sure ngrok is installed and configured ngrok http 3000 # Match the PORT in your .env fileForwardingURL (e.g.,https://<unique-code>.ngrok-free.app). Copy thehttpsURL. Keep ngrok running.Inbound URLandStatus URLfields, appending the paths we'll create in our Express app:YOUR_NGROK_HTTPS_URL/webhooks/inboundYOUR_NGROK_HTTPS_URL/webhooks/status(Example:https://abcdef123456.ngrok-free.app/webhooks/inbound)Generate new application.VONAGE_APPLICATION_IDfield in your.envfile.Link virtual numbersand link the Vonage virtual number you purchased for SMS. This routes incoming SMS for that number to this application's inbound webhook.2.2 Set Default SMS API (Important):
Ensure your account uses the Messages API (not the older SMS API) for webhooks.
API Settings.Default SMS Setting, selectMessages API.Save changes.2.3 Set Up Vonage WhatsApp Sandbox:
The Sandbox provides a testing environment without needing a dedicated WhatsApp Business number initially.
Messages SandboxunderDeveloper Toolsin the Dashboard menu.14157386102) and paste it intoVONAGE_WHATSAPP_NUMBERin your.envfile.Webhookson the Sandbox page:YOUR_NGROK_HTTPS_URL/webhooks/inbound(Same as application)YOUR_NGROK_HTTPS_URL/webhooks/status(Same as application)Save webhooks.2.4 Obtain Signature Secret:
API key.Edit(pencil icon) next to the API key.Signature secretvalue.VONAGE_SIGNATURE_SECRETin your.envfile.Your
.envfile should now be populated with real values.3. Implementing the Express Server and Core Logic
Now, let's write the Node.js code.
index.js:// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const { WhatsAppText } = require('@vonage/messages'); const { verifySignature } = require('@vonage/jwt'); // --- Initialization --- const app = express(); app.use(express.json()); // Middleware to parse JSON request bodies app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded bodies const PORT = process.env.PORT || 3000; // Initialize Vonage Client. Authentication for sending Messages API calls primarily uses // Application ID and Private Key to generate JWTs. API Key/Secret may be used for // other SDK functions or legacy APIs. The Signature Secret is used for webhook verification. 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, }); // --- Security Middleware: Verify Vonage Signature --- // Protects webhooks from unauthorized access const verifyVonageSignature = (req, res, next) => { try { // Extract token from 'Authorization: Bearer <token>' header const token = req.headers.authorization?.split(' ')[1]; if (!token) { console.warn('[WARN] Webhook Error: Missing Authorization header.'); return res.status(401).send('Unauthorized: Missing token'); } // Verify using the secret from .env const isValid = verifySignature(token, process.env.VONAGE_SIGNATURE_SECRET); if (isValid) { console.log('[INFO] Webhook Signature Verified Successfully.'); next(); // Proceed to the route handler } else { console.warn('[WARN] Webhook Error: Invalid Signature.'); res.status(401).send('Unauthorized: Invalid signature'); } } catch (error) { console.error('[ERROR] Webhook Error: Signature verification failed:', error); res.status(500).send('Internal Server Error'); } }; // --- Webhook Endpoints --- // Handles incoming messages (SMS & WhatsApp) app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => { console.log('[INFO] ------------------------------------'); console.log('[INFO] Incoming Message Webhook Received:'); console.log('[DEBUG] Timestamp:', new Date().toISOString()); console.log('[DEBUG] Body:', JSON.stringify(req.body, null, 2)); // Log the full payload const { from, to, channel, message_type, text, image } = req.body; // Basic validation if (!from || !channel || !message_type) { console.warn('[WARN] Inbound Webhook: Received incomplete data.'); return res.status(400).send('Bad Request: Missing required fields.'); } console.log(`[INFO] Message received on ${channel} from ${from.number || from.id} to ${to.number || to.id}`); // --- Channel-Specific Logic --- if (channel === 'sms') { console.log(`[INFO] SMS Received: "${text}"`); // TODO: Add your custom business logic here (e.g., database interaction, analytics, specific replies). // Example: Store message, trigger automated reply, etc. } else if (channel === 'whatsapp') { if (message_type === 'text') { console.log(`[INFO] WhatsApp Text Received: "${text}"`); // TODO: Add your custom business logic for WhatsApp text here. } else if (message_type === 'image') { console.log(`[INFO] WhatsApp Image Received: URL=${image.url}`); // TODO: Add your custom business logic for WhatsApp images here. // Note: Image URLs might be temporary. Download if needed promptly. } else { console.log(`[INFO] WhatsApp Message Type Received: ${message_type}`); // TODO: Handle other WhatsApp types (audio, video, file, location, etc.) as needed. } // Example: Send a confirmation reply back via WhatsApp sendWhatsAppConfirmation(from.id); // Use from.id for WhatsApp replies } else { console.log(`[WARN] Received message on unhandled channel: ${channel}`); } // Vonage expects a 200 OK response to acknowledge receipt res.status(200).send('OK'); console.log('[INFO] ------------------------------------'); }); // Handles message status updates (e.g., delivered, read) app.post('/webhooks/status', verifyVonageSignature, (req, res) => { console.log('[INFO] ------------------------------------'); console.log('[INFO] Status Webhook Received:'); console.log('[DEBUG] Timestamp:', new Date().toISOString()); console.log('[DEBUG] Body:', JSON.stringify(req.body, null, 2)); // Log the full payload const { message_uuid, status, timestamp, channel, to, from, error } = req.body; console.log(`[INFO] Status update for message ${message_uuid} (${channel}): ${status}`); if (error) { console.error(`[ERROR] Message Error for ${message_uuid}: Type=${error.type}, Reason=${error.reason}`); } // TODO: Add logic here to update message status in your database or tracking system. res.status(200).send('OK'); // Acknowledge receipt console.log('[INFO] ------------------------------------'); }); // --- API Endpoint for Sending Messages --- // Example: POST /send-message with JSON body: // { "channel": "sms", "to": "15551234567", "text": "Hello via SMS!" } // { "channel": "whatsapp", "to": "15551234567", "text": "Hello via WhatsApp!" } app.post('/send-message', async (req, res) => { console.log('[INFO] ------------------------------------'); console.log('[INFO] Send Message API Request Received:'); const { channel, to, text } = req.body; // Basic Input Validation if (!channel || !to || !text) { console.error('[ERROR] Send API Error: Missing channel, to, or text in request body.'); return res.status(400).json({ error: 'Missing required fields: channel, to, text' }); } if (!['sms', 'whatsapp'].includes(channel)) { console.error(`[ERROR] Send API Error: Invalid channel specified: ${channel}`); return res.status(400).json({ error: 'Invalid channel. Must be "sms" or "whatsapp".' }); } // Determine the 'from' number based on the channel const fromNumber = channel === 'sms' ? process.env.VONAGE_SMS_NUMBER : process.env.VONAGE_WHATSAPP_NUMBER; if (!fromNumber) { console.error(`[ERROR] Send API Error: Missing Vonage number for channel ${channel} in .env`); return res.status(500).json({ error: `Configuration error: Vonage number for ${channel} not set.` }); } console.log(`[INFO] Attempting to send ${channel} message to ${to} from ${fromNumber}`); try { let response; if (channel === 'sms') { response = await vonage.messages.send({ message_type: "text", text: text, to: to, from: fromNumber, channel: "sms" }); } else if (channel === 'whatsapp') { // For WhatsApp, use the specific WhatsAppText class response = await vonage.messages.send( new WhatsAppText({ text: text, to: to, // Recipient's WhatsApp number (allowlisted in Sandbox) from: fromNumber, // Your Vonage WhatsApp Sandbox number }) ); } console.log(`[INFO] Message sent successfully via ${channel}!`); console.log('[DEBUG] API Response:', response); // Includes message_uuid res.status(200).json({ success: true, message_uuid: response.message_uuid }); } catch (error) { console.error(`[ERROR] Error sending ${channel} message to ${to}:`, error.response ? JSON.stringify(error.response.data, null, 2) : error.message); // Provide more detail if available from Vonage error response const errorDetails = error.response?.data || { message: error.message }; res.status(error.response?.status || 500).json({ success: false, error: `Failed to send ${channel} message.`, details: errorDetails }); } console.log('[INFO] ------------------------------------'); }); // --- Helper Function (Example for WhatsApp Reply) --- async function sendWhatsAppConfirmation(recipientNumber) { console.log(`[INFO] Sending WhatsApp confirmation to ${recipientNumber}...`); try { const response = await vonage.messages.send( new WhatsAppText({ text: "*Message received!*", // Example using Markdown for italics in WhatsApp to: recipientNumber, from: process.env.VONAGE_WHATSAPP_NUMBER, }) ); console.log(`[INFO] WhatsApp confirmation sent: ${response.message_uuid}`); } catch (error) { console.error(`[ERROR] Error sending WhatsApp confirmation to ${recipientNumber}:`, error.response ? JSON.stringify(error.response.data, null, 2) : error.message); } } // --- Start Server --- app.listen(PORT, () => { console.log(`[INFO] Server listening on port ${PORT}`); console.log(`[INFO] Ensure your ngrok tunnel's HTTPS URL is configured for Vonage webhooks.`); console.log(`[INFO] Webhooks in Vonage Dashboard/Sandbox must point to YOUR_NGROK_HTTPS_URL/webhooks/...`); }); // --- Basic Error Handling (Catch-all) --- app.use((err, req, res, next) => { console.error("[ERROR] Unhandled Error:", err.stack || err); res.status(500).send('Something broke!'); });Code Explanation:
.env, requires modules, initializes Express and the Vonage SDK.express.json()andexpress.urlencoded()to parse incoming request bodies.verifyVonageSignatureMiddleware: This is crucial for webhook security. It extracts the JWT from theAuthorization: Bearer <token>header sent by Vonage, verifies it against yourVONAGE_SIGNATURE_SECRETusing@vonage/jwt. If invalid, it rejects the request with a 401 Unauthorized status./webhooks/inboundRoute:verifyVonageSignature.from,to,channel,message_type,text.smsandwhatsappchannels.// TODO:) explicitly marked for implementing your specific business logic (e.g., database storage, triggering workflows).sendWhatsAppConfirmationupon receiving a WhatsApp message.200 OKresponse. Vonage needs this acknowledgment; otherwise, it will retry sending the webhook, potentially causing duplicate processing./webhooks/statusRoute:verifyVonageSignature.delivered,read,failed).// TODO:) for updating message status in your system.200 OKresponse./send-messageAPI Route:channel,to, andtextin the JSON request body.fromnumber based on the channel.vonage.messages.send():WhatsAppTextclass helper from@vonage/messages.try...catchfor error handling during the API call, logging detailed errors.message_uuidon success or error details on failure.sendWhatsAppConfirmationHelper: A simple example function demonstrating how to send a reply back to a WhatsApp user. Note the use of Markdown (*...*) within the text string for formatting in WhatsApp.PORT, provides informative startup logs.4. Running and Testing the Application
4.1 Start the Server:
Ensure your ngrok tunnel (from step 2.1.6) is still running in one terminal. In your main project terminal, run:
You should see output similar to:
(Replace
YOUR_NGROK_HTTPS_URLin the Vonage settings with the actual URL provided by ngrok).4.2 Testing Inbound SMS:
VONAGE_SMS_NUMBER).node index.js. You should see logs for:[INFO] Webhook Signature Verified Successfully.[INFO] Incoming Message Webhook Received:(with SMS details)[INFO] SMS Received: ""Your message text""4.3 Testing Inbound WhatsApp:
VONAGE_WHATSAPP_NUMBER).node index.js. You should see logs for:[INFO] Webhook Signature Verified Successfully.[INFO] Incoming Message Webhook Received:(with WhatsApp details)[INFO] WhatsApp Text Received: ""Your message text""[INFO] Sending WhatsApp confirmation to <your_whatsapp_number>...[INFO] WhatsApp confirmation sent: <message_uuid>4.4 Testing Outbound API (
/send-message):Use
curlor a tool like Postman/Insomnia to send requests to your local server's API endpoint (http://localhost:3000/send-message).Send SMS:
curl -X POST http://localhost:3000/send-message \ -H ""Content-Type: application/json"" \ -d '{ ""channel"": ""sms"", ""to"": ""YOUR_PERSONAL_PHONE_NUMBER"", ""text"": ""Hello from Node.js via Vonage SMS API!"" }'(Replace
YOUR_PERSONAL_PHONE_NUMBERwith your actual number, including country code, e.g.,15551234567)Send WhatsApp:
curl -X POST http://localhost:3000/send-message \ -H ""Content-Type: application/json"" \ -d '{ ""channel"": ""whatsapp"", ""to"": ""YOUR_ALLOWLISTED_WHATSAPP_NUMBER"", ""text"": ""Hello from Node.js via Vonage WhatsApp Sandbox!"" }'(Replace
YOUR_ALLOWLISTED_WHATSAPP_NUMBERwith your number allowlisted in the Sandbox, including country code)4.5 Testing Status Webhook:
[INFO] Status Webhook Received:in your terminal as Vonage sends updates (e.g.,submitted,delivered,read- 'read' status depends on recipient settings and channel).4.6 Testing Error Conditions (Recommended):
/send-messageendpoint. Observe the error logged in the console and the API response.curlto send a POST request directly to your/webhooks/inboundngrok URL without a validAuthorization: Bearer <token>header, or with an invalid token. Verify your server responds with a401 Unauthorizedand logs a signature warning.# Example (should fail with 401) curl -X POST YOUR_NGROK_HTTPS_URL/webhooks/inbound \ -H ""Content-Type: application/json"" \ -d '{""from"": {""type"": ""sms"", ""number"": ""15550001111""}, ""to"": {""type"": ""sms"", ""number"": ""15552223333""}, ""channel"": ""sms"", ""message_type"": ""text"", ""text"": ""Test""}'402 Payment Requirederrors from the Vonage API if your account runs out of credit. Ensure your error handling logs these appropriately.5. Error Handling and Logging
console.log,console.warn,console.errorwith text prefixes ([INFO],[WARN],[ERROR],[DEBUG]).try...catchblocks wrap Vonage API calls and webhook processing logic. The JWT verification middleware handles signature errors. A basic Express error handler catches unhandled exceptions.WinstonorPinofor structured JSON logs, which are easier to parse and analyze in log management systems (e.g., Datadog, Splunk, ELK stack). Include timestamps, log levels, request IDs (using middleware likeexpress-request-id), and relevant context (likemessage_uuid).error.response.data.typeorerror.response.status(e.g., invalid number format400, insufficient funds402, authentication issues401) and provide more informative responses or implement specific retry logic where appropriate.401, server crashes500, authentication errors401on sending).6. Security Considerations
VONAGE_SIGNATURE_SECRETsecure and treat it like a password. Rotate it if compromised..envfor local development and secure environment variable management in production (e.g., platform secrets, HashiCorp Vault, AWS Secrets Manager, Google Secret Manager). See Section 9 for private key handling.400 Bad Request)./send-messageAPI: Basic validation is included. Enhance it based on expected inputs (e.g., max text length, valid number formats using libraries likelibphonenumber-js). Use libraries likeJoiorexpress-validatorfor defining and enforcing complex validation schemas./send-messageAPI endpoint (and potentially webhooks, though less common) from abuse or accidental loops. Use middleware likeexpress-rate-limit.npm auditor tools like Snyk, and update them promptly. Implementnpm audit fixinto your development workflow.VONAGE_PRIVATE_KEYpath points to a file locally. Do not commit theprivate.keyfile to version control. For production, avoid storing the key as a file on the server filesystem if possible. Best practices include:private.keyfile in a secure environment variable (potentially base64 encoded for easier handling in some systems). Read the key content from this variable inindex.jsinstead of a file path.7. Database Integration (Conceptual)
This guide focuses on the core messaging logic. For persistent storage:
messages(storingmessage_uuid,vonage_message_uuid,channel,direction['inbound'/'outbound'],sender_id,recipient_id,content[text/URL],status,error_info,timestamp_received,timestamp_sent,timestamp_status_update) and potentiallyconversationsorusers.Prisma,Sequelize(SQL), orMongoose(MongoDB) to interact with your chosen database./webhooks/inbound: After validation, create a new record in yourmessagestable with the incoming message details./webhooks/status: Find the message record using themessage_uuidand update itsstatus,timestamp_status_update, and potentiallyerror_info./send-message: Before sending the message via Vonage API, create a record in yourmessagestable (with initial status like 'pending' or 'submitted'). After a successful API call, update the record with thevonage_message_uuidreturned by Vonage. If the API call fails immediately, log the error in the record.8. Troubleshooting and Caveats
VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY(content or path), andVONAGE_SIGNATURE_SECRETin your environment. Ensure theprivate.keyfile exists and is readable if using a file path, or that the environment variable contains the correct key content.VONAGE_SIGNATURE_SECRETin your environment matches the one in your Vonage Dashboard settings exactly. Copy/paste carefully.req.headersin logs if needed).Inbound URLandStatus URLin your Vonage Application settings exactly match your current ngrok HTTPS URL plus the correct path (/webhooks/inboundor/webhooks/status).node index.js) and hasn't crashed. Check console logs for errors.