Sending and receiving SMS & WhatsApp messages with Node.js and Vonage
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 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:
- 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
: Installngrok
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.
mkdir vonage-node-messaging cd vonage-node-messaging
-
Initialize npm: Initialize the project using npm. The
-y
flag accepts the default settings.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.npm install express @vonage/server-sdk dotenv
-
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 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
: Addnode_modules
and.env
to your.gitignore
file to prevent committing dependencies and sensitive credentials.# .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 # 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 theprivate.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 thenode index.js
ornode 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
).
send.js
)
Sending Outbound Messages (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:
- 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 toMESSAGES_SANDBOX_URL_MESSAGES
. This directs WhatsApp API calls to the sandbox endpoint. ThisapiHost
line must be removed when deploying your application to send live WhatsApp messages. sendSms
: Usesvonage.messages.send
withchannel: 'sms'
and your purchasedVONAGE_SMS_FROM_NUMBER
. Note that this uses the same client; ensure the sandboxapiHost
doesn't interfere with SMS if you encounter issues (though typically it doesn't).sendWhatsAppMessage
: Usesvonage.messages.send
withchannel: 'whatsapp'
and theVONAGE_WHATSAPP_SANDBOX_NUMBER
as thefrom
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.
- We load environment variables using
index.js
)
Receiving Inbound Messages & Status Updates (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:
- Loads environment variables and initializes Express.
- Uses
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 OK
response. Crucially, Vonage requires a quick200 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 responds200 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:
// 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 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.
-
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.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 configuredPORT
) to the internet.# Make sure your Node.js server is running in another terminal: node index.js ngrok http 3000
ngrok
will provide aForwarding
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 restartngrok
(with the free plan). -
Create a Vonage Application:
- Go to your Vonage API Dashboard.
- 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 updateVONAGE_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
- Inbound URL: Paste your
- Click ""Generate new application"".
- On the next screen, you'll see the Application ID. Copy this and paste it into your
.env
file asVONAGE_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 asVONAGE_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"".
- Inbound URL: Enter your
- The
VONAGE_WHATSAPP_SANDBOX_NUMBER
in your.env
should already be14157386102
. - 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
andVONAGE_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 insend.js
around thevonage.messages.send
calls. 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...catch
within the webhook handlers for any complex processing logic that might fail. - Logging: We are using
console.log
andconsole.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.- Example with Pino (Conceptual):
// 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');
- Example with Pino (Conceptual):
- 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 a200 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.
- Your Responsibility: Ensure your webhook handlers are fast and reliably return
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.- Inbound (
/webhooks/inbound
): Parsereq.body
, create a new record in themessages
table withmessage_direction='inbound'
,status='accepted'
, populate fields, and save. - Status (
/webhooks/status
): Parsereq.body
, find the message record bymessage_uuid
, update itsstatus
,vonage_status_timestamp
, and potentiallyerror_code
/error_reason
. - Outbound (
send.js
or 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
.
- Inbound (
-
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
asVONAGE_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.
- Find your ""Signature secret"" in the Vonage Dashboard -> Settings page (associated with your API Key). Add it to
- 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 likeexpress-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).