code examples
code examples
Sending and Receiving SMS & WhatsApp with Node.js and Vonage
A guide to building a Node.js application using Express and the Vonage Messages API to handle SMS and WhatsApp messaging, including setup, sending, receiving via webhooks, and configuration.
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.envfile intoprocess.env. Essential for securely managing API keys and configuration.ngrok: A tool to expose local development servers to the internet. Crucial for testing Vonage webhooks during development.
System Architecture:
The system involves the following flow:
- 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: Installngrokand 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.
bashmkdir vonage-node-messaging cd vonage-node-messaging -
Initialize npm: Initialize the project using npm. The
-yflag accepts the default settings.bashnpm init -yThis creates a
package.jsonfile. -
Install Dependencies: Install Express for the web server, the Vonage Server SDK, and
dotenvfor environment variable management.bashnpm install express @vonage/server-sdk dotenv -
Create Project Structure: Create the basic files and directories we'll need.
bash# On macOS / Linux touch index.js send.js .env .gitignore # On Windows (Command Prompt) type nul > index.js type nul > send.js type nul > .env type nul > .gitignore # Or Windows (PowerShell) New-Item index.js -ItemType File New-Item send.js -ItemType File New-Item .env -ItemType File New-Item .gitignore -ItemType Fileindex.js: Our main Express server file for handling incoming webhooks.send.js: A script to demonstrate sending outbound messages..env: Stores sensitive credentials and configuration..gitignore: Specifies intentionally untracked files that Git should ignore.
-
Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing dependencies and sensitive credentials.text# .gitignore node_modules/ .env *.key # Also ignore private key files if stored locally -
Set Up Environment Variables (
.env): Open the.envfile and add the following variables. You will populate these with your actual credentials later.dotenv# .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=3000Explanation of Variables:
VONAGE_API_KEY,VONAGE_API_SECRET: Your primary credentials for authenticating API requests. Find these on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Unique ID for the Vonage Application you will create to handle messages.VONAGE_PRIVATE_KEY_PATH: The file path to theprivate.keyfile downloaded when creating the Vonage Application. Authentication using Application ID and Private Key is required for the Messages API. Note: The path./private.keyassumes the key file is in the same directory from which you run thenode index.jsornode send.jscommand. Adjust the path accordingly if the file is located elsewhere relative to your execution context.VONAGE_SMS_FROM_NUMBER: The Vonage virtual number you purchased and linked to your application. This is the sender ID for outbound SMS. Use E.164 format without the leading+(e.g.,14155551212).VONAGE_WHATSAPP_SANDBOX_NUMBER: The shared Vonage number used by the WhatsApp Sandbox for testing (14157386102).YOUR_TARGET_PHONE_NUMBER: Your personal mobile number (SMS and WhatsApp capable) used as the recipient for testing outbound messages. Use E.164 format without the leading+.PORT: The port your local Express server will run on.
2. Implementing core functionality
We'll split the core logic into two parts: sending messages (send.js) and receiving messages (index.js).
Sending Outbound Messages (send.js)
This script demonstrates how to send both SMS and WhatsApp messages using the Vonage SDK.
-
Edit
send.js: Add the following code tosend.js.javascript// 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
Vonageclient using the Application ID and the path to the private key file. - Crucially, the
apiHostoption is set toMESSAGES_SANDBOX_URL_MESSAGES. This directs WhatsApp API calls to the sandbox endpoint. ThisapiHostline must be removed when deploying your application to send live WhatsApp messages. sendSms: Usesvonage.messages.sendwithchannel: 'sms'and your purchasedVONAGE_SMS_FROM_NUMBER. Note that this uses the same client; ensure the sandboxapiHostdoesn't interfere with SMS if you encounter issues (though typically it doesn't).sendWhatsAppMessage: Usesvonage.messages.sendwithchannel: 'whatsapp'and theVONAGE_WHATSAPP_SANDBOX_NUMBERas thefromnumber. It includes error handling specific to the sandbox (e.g., whitelisting check).- The
mainfunction orchestrates sending both types of messages. - Error handling is included using
try...catchblocks, logging detailed errors.
- We load environment variables using
Receiving Inbound Messages & Status Updates (index.js)
This script sets up an Express server to listen for incoming webhook requests from Vonage.
-
Edit
index.js: Add the following code toindex.js.javascript// 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 OKresponse. Crucially, Vonage requires a quick200 OKresponse to prevent webhook retries./webhooks/status: Handles POST requests from Vonage about the status of messages you sent (e.g., delivered, failed, read). It also logs the payload and responds200 OK./health: A simple GET endpoint to check if the server is running.- 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:
bash# 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:
bash# Replace placeholders with valid values (UUID from actual sending, E.164 numbers without '+') curl -X POST http://localhost:3000/webhooks/status \ -H ""Content-Type: application/json"" \ -d '{ ""message_uuid"": ""actual-message-uuid-from-sending"", ""to"": ""YOUR_TARGET_PHONE_NUMBER_E164"", ""from"": ""VONAGE_WHATSAPP_SANDBOX_NUMBER"", ""channel"": ""whatsapp"", ""timestamp"": ""2025-04-20T10:05:00Z"", ""status"": ""delivered"", ""usage"": { ""currency"": ""EUR"", ""price"": ""0.0050"" } }'Check your
index.jsconsole output to verify the logs.
4. Integrating with Vonage (Dashboard Setup)
Now, let's configure Vonage to connect to our application.
-
Start
ngrok: Expose your local server (running on port 3000, or your configuredPORT) to the internet.bash# Make sure your Node.js server is running in another terminal: node index.js ngrok http 3000ngrokwill provide aForwardingURL ending in.ngrok-free.appor similar (e.g.,https://abcd-1234-efgh.ngrok-free.app). Copy this HTTPS URL. This URL changes every time you restartngrok(with the free plan). -
Create a Vonage Application:
- 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.keyfile will be downloaded. Save this file in your project's root directory (or updateVONAGE_PRIVATE_KEY_PATHin.envif you save it elsewhere). - Crucially, enable the ""Messages"" capability.
- Configure the Webhooks:
- Inbound URL: Paste your
ngrokHTTPS URL and append/webhooks/inbound. Example:https://abcd-1234-efgh.ngrok-free.app/webhooks/inbound - Status URL: Paste your
ngrokHTTPS 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
.envfile 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
.envfile 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
ngrokURL +/webhooks/inbound. - Status URL: Enter your
ngrokURL +/webhooks/status. - Click ""Save webhooks"".
- Inbound URL: Enter your
- The
VONAGE_WHATSAPP_SANDBOX_NUMBERin your.envshould 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_KEYandVONAGE_API_SECRETin your.envfile.
-
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...catchblocks insend.jsaround thevonage.messages.sendcalls. These catch errors during the API request (e.g., network issues, invalid credentials, invalid numbers). The error details (often found inerr.response.data) are logged. Inindex.js, basic logging exists, but in production, you'd addtry...catchwithin the webhook handlers for any complex processing logic that might fail. - Logging: We are using
console.logandconsole.error. For production, use a structured logging library like Pino or Winston. This allows for leveled logging (debug, info, warn, error), JSON formatting (easier for log aggregation tools), and potentially sending logs to external services.- Example with Pino (Conceptual):
javascript
// 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/inboundor/webhooks/statusendpoint doesn't respond with a200 OKstatus within a short timeout (typically a few seconds), Vonage will retry sending the webhook notification several times with increasing delays (exponential backoff).- 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):
sqlCREATE 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 themessagestable 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.jsor API endpoint): Before callingvonage.messages.send, create a record withmessage_direction='outbound',status='submitted'. After the API call succeeds, update the record with the returnedmessage_uuid.
- 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 (
.envlocally, secure configuration management in deployment). Ensure.envand*.keyare 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
.envasVONAGE_SIGNATURE_SECRET. - Important: The implementation details for verifying the signature depend heavily on the current
@vonage/server-sdkversion and Vonage's specific signature scheme (e.g., JWT or header-based HMAC). Consult the official Vonage Node.js SDK documentation and developer portal for the current recommended method for signature verification within an Express application. You'll typically need middleware to perform this check before processing the webhook body.
- 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 (
ngrokprovides this for local testing). In production, ensure your server is configured for HTTPS using TLS certificates (e.g., via Let's Encrypt or your hosting provider).
Frequently Asked Questions
How to send SMS messages with Node.js and Vonage?
Use the Vonage Messages API with the Node.js SDK. After initializing the Vonage client with your API credentials, call `vonage.messages.send` with the recipient's number, your Vonage number as the sender, and the message text. Ensure your Vonage number is SMS-enabled in your dashboard.
What is the Vonage Messages API?
The Vonage Messages API is a unified platform for sending and receiving messages across multiple channels, including SMS, WhatsApp, Viber, and Facebook Messenger. It simplifies multi-channel communication by providing a single API for various messaging platforms.
Why does Vonage use a WhatsApp Sandbox?
The Vonage WhatsApp Sandbox provides a testing environment for developing and experimenting with WhatsApp integrations without incurring real-world costs or needing full business approval. You can whitelist your own number for testing purposes.
When should I remove the apiHost setting in my Vonage client?
Remove the `apiHost: MESSAGES_SANDBOX_URL_MESSAGES` configuration from your Vonage client *only* when deploying to production for live WhatsApp messaging. This setting is crucial for directing WhatsApp traffic to the Sandbox during development.
Can I receive WhatsApp messages locally with Node.js?
Yes, you can receive WhatsApp messages locally during development. Use ngrok to create a public, secure URL for your local server that Vonage can send webhooks to. Configure this ngrok URL in your Vonage application's webhook settings, specifically for the WhatsApp Sandbox.
How to set up Vonage webhooks for SMS and WhatsApp?
In the Vonage Dashboard, create a new application and enable the Messages capability. Provide the URLs for your inbound and status webhooks, ensuring they point to your public server address (e.g., ngrok URL during development) appended with '/webhooks/inbound' and '/webhooks/status'. Link your Vonage virtual number to this application.
What is the Vonage Application ID used for?
The Vonage Application ID uniquely identifies your application within the Vonage platform and is essential for authentication when using the Messages API with an associated private key. You'll need both the Application ID and the corresponding private key to initialize the Vonage client in your Node.js code.
How to handle Vonage webhook retries?
Vonage automatically retries webhook requests if your server doesn't respond with a 200 OK within a short timeframe. Ensure your webhook handlers execute quickly and return 200 OK. Process time-consuming tasks asynchronously using background jobs. Implement idempotency checks (e.g., using message_uuid) to prevent duplicate actions on retries.
What is ngrok and why is it needed?
ngrok creates secure tunnels from public URLs to your local machine, allowing Vonage to send webhooks to your development server. This is crucial for testing webhooks before deploying your application.
How to secure Vonage API credentials in Node.js?
Store API keys, secrets, and private keys in environment variables (`.env` file locally) and never commit them to version control. Use secure configuration management systems in production.
How to whitelist a number for Vonage WhatsApp Sandbox?
Go to the Messages API Sandbox in your Vonage Dashboard. Scan the QR code with your WhatsApp app, or send the specified message from your phone's WhatsApp to the provided number. Follow the subsequent instructions to complete the whitelisting.
What are common WhatsApp Sandbox errors?
A frequent error is "Forbidden," indicating the recipient isn't whitelisted. Ensure the target number is correctly added to the Sandbox. Other errors might relate to incorrect `apiHost` settings or using a non-sandbox number as the 'from' address in WhatsApp messages.
How to structure a database for storing Vonage messages?
A recommended schema includes fields for message_uuid, channel, direction, sender/recipient IDs, message content, status, timestamps, and error details. Consider indexing columns like sender/recipient, status, and channel for efficient querying.
What are Vonage message status updates?
Status updates provide real-time delivery information about your outbound messages (e.g., 'delivered', 'failed', 'read'). Vonage sends these updates to your status webhook URL, where your application can update the message status in its database accordingly.
How to verify the signature of Vonage webhook requests?
Enable signature verification in your Vonage Dashboard settings to enhance security. Refer to the Vonage Node.js SDK documentation for the latest implementation details, which often involve middleware using your signature secret to validate the request's authenticity.