This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to send SMS and WhatsApp messages via the Vonage Messages API. We will cover project setup, sending messages through different channels, handling incoming messages and status updates via webhooks, and implementing essential production considerations like security and error handling.
By the end of this tutorial, you will have a functional application capable of programmatically sending SMS and WhatsApp messages and receiving delivery status updates and inbound messages from users. This provides a foundation for building chatbots, notification systems, two-factor authentication flows, and other communication-driven features.
Project Overview and Goals
What We'll Build:
A Node.js Express server.
Integration with the Vonage Messages API using the official Node.js SDK.
Functionality to send outbound SMS messages.
Functionality to send outbound WhatsApp messages (using the Vonage Sandbox initially).
Webhook endpoints to receive inbound messages from users (SMS and WhatsApp).
Webhook endpoints to receive message status updates (e.g., delivered, failed).
Secure handling of API credentials and webhook signature verification.
Problem Solved:
This application provides a unified way to interact with customers or users over two popular messaging channels – SMS and WhatsApp – directly from your Node.js backend. It abstracts the complexities of the Vonage API into reusable functions and provides a basic structure for handling asynchronous communication events.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications. (Version 18 or higher recommended).
Express: A minimal and flexible Node.js web application framework used to build the server and API endpoints/webhooks.
Vonage Messages API: A powerful API enabling communication across multiple channels (SMS, MMS, WhatsApp, Facebook Messenger, Viber) through a single interface.
Vonage Node.js Server SDK (@vonage/server-sdk): Simplifies interaction with Vonage APIs, including authentication and sending messages.
Vonage Messages SDK (@vonage/messages): Provides convenient classes for constructing messages for specific channels (like WhatsAppText).
Vonage JWT SDK (@vonage/jwt): Used for verifying the signature of incoming webhook requests to ensure they originate from Vonage.
dotenv: A module to load environment variables from a .env file into process.env.
ngrok: A tool to expose local servers to the internet, necessary for testing webhooks during development.
Give your application a name (e.g., ""Node Express Messaging"").
Click Generate public and private key. This will automatically download a private.key file. Save this file securely in the root of your project directory (e.g., alongside package.json). Crucially, ensure private.key is listed in your .gitignore file and is NEVER committed to version control. For production, load this key securely (e.g., from environment variables, secrets management, or a secure volume mount) rather than directly from the filesystem. The public key is stored by Vonage.
Enable the Messages capability by toggling it on.
You'll see fields for Inbound URL and Status URL. We'll fill these in the next step after starting ngrok. Leave them blank for now or use temporary placeholders like http://localhost.
Click Generate new application.
On the next screen, Link the Vonage virtual number you purchased earlier to this application. Find your number and click the ""Link"" button.
Note down the Application ID displayed on this page.
Configure SMS API Settings:
Ensure your account uses the Messages API for SMS by default, as recommended by Vonage documentation for new integrations.
In the Vonage Dashboard, navigate to API Settings.
Scroll down to the SMS Settings section.
Under Default SMS Setting, select Messages API.
Click Save changes.
Set up WhatsApp Sandbox (for Testing):
For testing WhatsApp without a full business setup, use the Vonage Sandbox.
Navigate to Messages API Sandbox in the Vonage Dashboard.
Follow the instructions to activate the Sandbox by sending a specific message from your WhatsApp number to the provided Sandbox number. This ""allowlists"" your number for testing.
Keep this page open, as you'll need the Sandbox number and will configure webhooks here too.
Start ngrok:
To receive webhooks from Vonage on your local machine, you need to expose your local server to the internet.
Open a new terminal window (keep your project terminal open).
Run ngrok, specifying the port your Express server will listen on (we'll use 3000):
bash
# Make sure you've authenticated ngrok if it's your first time# ngrok config add-authtoken YOUR_AUTH_TOKENngrok http 3000
ngrok will display output including a Forwarding URL that looks like https://<random-string>.ngrok-free.app (or similar, depending on your plan/version). Copy this HTTPS URL.
Configure Webhook URLs in Vonage:
Vonage Application: Go back to your application settings in the Vonage Dashboard (Applications -> Your Application Name).
Enter your ngrok HTTPS URL followed by /webhooks/inbound into the Inbound URL field (e.g., https://<random-string>.ngrok-free.app/webhooks/inbound).
Enter your ngrok HTTPS URL followed by /webhooks/status into the Status URL field (e.g., https://<random-string>.ngrok-free.app/webhooks/status).
Click Save changes.
WhatsApp Sandbox: Go back to the Messages API Sandbox page in the Vonage Dashboard.
Enter the same URLs (.../webhooks/inbound and .../webhooks/status) into the respective webhook fields on the Sandbox configuration page.
Click Save webhooks.
Configure Environment Variables (.env):
Open the .env file you created and add the following variables, replacing the placeholder values with your actual credentials:
bash
# Vonage API CredentialsVONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application CredentialsVONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_PRIVATE_KEY=./private.key # Path relative to project root - load securely in production!# Vonage NumbersVONAGE_SMS_FROM_NUMBER=YOUR_VONAGE_SMS_NUMBER # Your purchased Vonage number in E.164 format (e.g., 14155550100)VONAGE_WHATSAPP_SANDBOX_NUMBER=14157386102# The Vonage WhatsApp Sandbox number# Webhook SecurityVONAGE_API_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET
# Server ConfigurationPORT=3000
VONAGE_API_KEY & VONAGE_API_SECRET: Found at the top of your Vonage API Dashboard.
VONAGE_APPLICATION_ID: The ID generated when you created the Vonage Application.
VONAGE_PRIVATE_KEY: The path to the private.key file you downloaded and saved in your project root. Ensure this path is correct relative to where you run the node process.
VONAGE_SMS_FROM_NUMBER: Your purchased Vonage virtual number (without + or leading 00).
VONAGE_WHATSAPP_SANDBOX_NUMBER: The specific number provided by the Vonage WhatsApp Sandbox. For production WhatsApp, this would be your registered WhatsApp Business number.
VONAGE_API_SIGNATURE_SECRET: Found in the Vonage API Dashboard under API Settings -> Webhook signature secret. Click ""Edit"" or ""Generate"" if needed.
PORT: The port your Express server will run on (must match the ngrok port).
2. Implementing core functionality
Now, let's write the code in src/server.js to initialize the server, configure Vonage, send messages, and handle webhooks.
javascript
// src/server.jsrequire('dotenv').config();// Load environment variables from .env file firstconst express =require('express');const{Vonage}=require('@vonage/server-sdk');const{WhatsAppText}=require('@vonage/messages');const{ verifySignature }=require('@vonage/jwt');// --- Initialization ---const app =express();// Note: We apply express.json() globally AFTER raw body middleware for webhook routes// app.use(express.json()); // Moved lowerapp.use(express.urlencoded({extended:true}));// Middleware to parse URL-encoded bodies// Initialize Vonage Client// Ensure all required environment variables are loaded correctlyif(!process.env.VONAGE_API_KEY||!process.env.VONAGE_API_SECRET||!process.env.VONAGE_APPLICATION_ID||!process.env.VONAGE_PRIVATE_KEY){console.error('ERROR: Missing Vonage API credentials in .env file.'); process.exit(1);// Exit if essential credentials are missing}if(!process.env.VONAGE_API_SIGNATURE_SECRET){// Signature secret is mandatory for webhook security.console.error('ERROR: Missing VONAGE_API_SIGNATURE_SECRET in .env file. Webhooks cannot be verified.'); process.exit(1);}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// Path to your private key file},{// Optional: Set custom logger or other options if needed// logger: customLogger,// Optional: Use the sandbox host for ALL messages if testing ONLY WhatsApp Sandbox features extensively// apiHost: 'https://messages-sandbox.nexmo.com' // NOTE: Remove/comment this for production SMS or non-sandbox WhatsApp});// --- Helper Functions ---/**
* Sends an SMS message using the Vonage Messages API.
* @param{string}toNumber - The recipient's phone number in E.164 format.
* @param{string}messageText - The text content of the message.
*/asyncfunctionsendSms(toNumber, messageText){console.log(`Attempting to send SMS to ${toNumber}...`);if(!process.env.VONAGE_SMS_FROM_NUMBER){console.error('ERROR: VONAGE_SMS_FROM_NUMBER is not set in .env');return;}try{const resp =await vonage.messages.send({message_type:""text"",text: messageText,to: toNumber,from: process.env.VONAGE_SMS_FROM_NUMBER,channel:""sms""});console.log(`SMS sent successfully to ${toNumber}. Message UUID: ${resp.messageUuid}`);}catch(err){console.error(`Error sending SMS to ${toNumber}:`, err?.response?.data || err.message);// Consider adding more robust error handling/retry logic here}}/**
* Sends a WhatsApp message using the Vonage Messages API.
* Uses the Sandbox number by default.
* @param{string}toNumber - The recipient's WhatsApp-enabled number in E.164 format (must be allowlisted in Sandbox).
* @param{string}messageText - The text content of the message.
*/asyncfunctionsendWhatsApp(toNumber, messageText){console.log(`Attempting to send WhatsApp message to ${toNumber} via Sandbox...`);if(!process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER){console.error('ERROR: VONAGE_WHATSAPP_SANDBOX_NUMBER is not set in .env');return;}try{// NOTE: For production, 'from' would be your registered WhatsApp Business number.// The host might also need adjustment if not using the global endpoint.// The 'apiHost' in the Vonage client initialization can also control this.const resp =await vonage.messages.send(newWhatsAppText({text: messageText,to: toNumber,from: process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER,// Optional: client_ref for your own tracking// client_ref: `my-whatsapp-message-${Date.now()}`})// Optional second argument for sandbox override if client wasn't initialized with sandbox host:// , { apiHost: 'https://messages-sandbox.nexmo.com' });console.log(`WhatsApp message sent successfully to ${toNumber}. Message UUID: ${resp.messageUuid}`);}catch(err){console.error(`Error sending WhatsApp message to ${toNumber}:`, err?.response?.data || err.message);// Consider adding more robust error handling/retry logic here}}/**
* Verifies the signature of an incoming Vonage webhook request.
* @param{express.Request}req - The Express request object.
* @returns{boolean} - True if the signature is valid, false otherwise.
*/functionverifyWebhookSignature(req){try{// IMPORTANT: Use raw body for signature verification// We need to configure Express to make the raw body available.// See rawBodyMiddleware configuration below.if(!req.rawBody){console.error('ERROR: Raw body not available for signature verification. Ensure rawBodyMiddleware is used.');returnfalse;}if(!process.env.VONAGE_API_SIGNATURE_SECRET){// This should ideally be caught at startup, but double-check here.console.error('ERROR: VONAGE_API_SIGNATURE_SECRET is not set. Cannot verify webhook signature.');// Fail verification if the secret is missing. Verification is mandatory for security.returnfalse;}// Clone headers and convert to lowercase keys as expected by verifySignatureconst headers ={};for(const key in req.headers){ headers[key.toLowerCase()]= req.headers[key];}// Extract token from Authorization header (format: ""Bearer <JWT>"")const token = headers['authorization']?.split(' ')[1];if(!token){console.error('Webhook Error: Authorization header missing or malformed.');returnfalse;}// Verify the signature using the raw bodyconst isValid =verifySignature(token, process.env.VONAGE_API_SIGNATURE_SECRET, req.rawBody.toString());if(!isValid){console.error('Webhook Error: Invalid Signature');}return isValid;}catch(error){console.error('Error verifying webhook signature:', error.message);returnfalse;}}// --- Middleware for Raw Body ---// This needs to run BEFORE express.json() for routes requiring signature verificationconst rawBodyMiddleware = express.raw({type:'application/json',verify:(req, res, buf)=>{// Store the raw buffer on the request object req.rawBody= buf;}});// --- Webhook Endpoints ---// Apply raw body parsing ONLY to webhook routes, THEN apply JSON parsingapp.post('/webhooks/inbound', rawBodyMiddleware, express.json(),async(req, res)=>{console.log('Received Inbound Webhook:');console.log(JSON.stringify(req.body,null,2));// Log the full body// 1. Verify Signature (CRITICAL for security)if(!verifyWebhookSignature(req)){console.log('Webhook signature verification failed. Responding 401.');return res.status(401).send('Invalid Signature');}console.log('Webhook signature verified successfully.');// 2. Process the message based on channelconst{from, channel, message }= req.body;if(channel ==='sms'|| channel ==='whatsapp'){const sender =from.number||from.id;// WhatsApp uses 'id', SMS uses 'number'const messageContent = message?.content?.text ||'[Non-text message content]';console.log(`Received ${channel.toUpperCase()} message from ${sender}: ""${messageContent}""`);// 3. (Optional) Send an automated replyconst replyText =`Thanks for your ${channel.toUpperCase()} message! We received: ""${messageContent}""`;if(channel ==='sms'&& process.env.VONAGE_SMS_FROM_NUMBER){// IMPORTANT: Check if 'from.number' exists before replying to SMSif(from.number){awaitsendSms(from.number, replyText);}else{console.warn('Cannot reply to SMS: Sender number missing in webhook payload.');}}elseif(channel ==='whatsapp'&& process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER){// IMPORTANT: Check if 'from.id' exists before replying to WhatsAppif(from.id){// Check if within 24-hour window for non-template messages (Sandbox usually allows replies)awaitsendWhatsApp(from.id, replyText);}else{console.warn('Cannot reply to WhatsApp: Sender ID missing in webhook payload.');}}}else{console.log(`Received webhook for unhandled channel: ${channel}`);}// 4. Acknowledge receipt with a 200 OK// Vonage webhooks expect a 200 OK response quickly, otherwise they retry. res.status(200).end();});app.post('/webhooks/status', rawBodyMiddleware, express.json(),(req, res)=>{console.log('Received Status Webhook:');console.log(JSON.stringify(req.body,null,2));// Log the full body// 1. Verify Signature (CRITICAL for security)if(!verifyWebhookSignature(req)){console.log('Webhook signature verification failed. Responding 401.');return res.status(401).send('Invalid Signature');}console.log('Webhook signature verified successfully.');// 2. Process the status update (e.g., update database, trigger alerts)const{ message_uuid, status, timestamp, error }= req.body;console.log(`Status update for Message UUID ${message_uuid}: ${status} at ${timestamp}`);if(error){console.error(`Error details: Code=${error.code}, Reason=${error.reason}`);}// 3. Acknowledge receipt with a 200 OK res.status(200).end();});// --- API Endpoints (Example) ---// Apply standard JSON parsing middleware to API routes (and any other non-webhook routes)app.use(express.json());// Note: The following comments use apidoc syntax (https://apidocjs.com/).// You can use the 'apidoc' tool to generate API documentation from these comments./**
* @api{post} /api/send/sms Send an SMS message
* @apiName SendSms
* @apiGroup Messages
*
* @apiBody{String} to Recipient phone number in E.164 format.
* @apiBody{String} message Text content of the SMS.
*
* @apiSuccess{String} status Success message.
* @apiError{String} error Error description.
*/app.post('/api/send/sms',async(req, res)=>{const{ to, message }= req.body;// Basic Input Validationif(!to ||!message){return res.status(400).json({error:'Missing ""to"" or ""message"" in request body.'});}// Basic format check (11-15 digits). For robust E.164 validation, use a library like libphonenumber-js.if(!/^\d{11,15}$/.test(to)){return res.status(400).json({error:'Invalid ""to"" number format. The Vonage SDK expects E.164 format without the leading \'+\'.'});}// TODO: Add Authentication/Authorization middleware here for productionconsole.log(`API request to send SMS to ${to}`);try{awaitsendSms(to, message); res.status(200).json({status:`SMS dispatch initiated to ${to}`});}catch(error){console.error(""APIError sending SMS:"", error); res.status(500).json({error:'Failed to send SMS.'});}});/**
* @api{post} /api/send/whatsapp Send a WhatsApp message (via Sandbox)
* @apiName SendWhatsApp
* @apiGroup Messages
*
* @apiBody{String} to Recipient WhatsApp number in E.164 format (must be allowlisted).
* @apiBody{String} message Text content of the message.
*
* @apiSuccess{String} status Success message.
* @apiError{String} error Error description.
*/app.post('/api/send/whatsapp',async(req, res)=>{const{ to, message }= req.body;// Basic Input Validationif(!to ||!message){return res.status(400).json({error:'Missing ""to"" or ""message"" in request body.'});}// Basic format check (11-15 digits). For robust E.164 validation, use a library like libphonenumber-js.if(!/^\d{11,15}$/.test(to)){return res.status(400).json({error:'Invalid ""to"" number format. The Vonage SDK expects E.164 format without the leading \'+\'.'});}// TODO: Add Authentication/Authorization middleware here for productionconsole.log(`API request to send WhatsApp to ${to}`);try{awaitsendWhatsApp(to, message);// Using the Sandbox number by default res.status(200).json({status:`WhatsApp dispatch initiated to ${to}`});}catch(error){console.error(""APIError sending WhatsApp:"", error); res.status(500).json({error:'Failed to send WhatsApp message.'});}});// --- Server Start ---const port = process.env.PORT||3000;app.listen(port,()=>{console.log(`Server listening on port ${port}`);console.log(`ngrok forwarding URL (ensure it matches Vonage webhook config): Check your ngrok terminal.`);// You can add a startup check here to ensure essential env vars are presentif(!process.env.VONAGE_SMS_FROM_NUMBER)console.warn('WARN: VONAGE_SMS_FROM_NUMBER not set. Cannot send SMS.');if(!process.env.VONAGE_WHATSAPP_SANDBOX_NUMBER)console.warn('WARN: VONAGE_WHATSAPP_SANDBOX_NUMBER not set. Cannot send WhatsApp via Sandbox.');// Note: VONAGE_API_SIGNATURE_SECRET absence is now a fatal error at startup.});
Explanation:
Dependencies & Setup: We import necessary modules and initialize Express. dotenv.config() loads variables from .env.
Vonage Client: We initialize the Vonage client using credentials from environment variables. Checks ensure essential credentials (including signature secret) exist, exiting if not. We comment out the apiHost override for the sandbox, preferring the default endpoint.
sendSms Function: Takes toNumber and messageText, constructs the payload for the Messages API (channel: 'sms'), and uses vonage.messages.send(). Basic logging for success and failure is included.
sendWhatsApp Function: Similar to sendSms, but uses the WhatsAppText class from @vonage/messages and specifies channel: 'whatsapp'. It uses the VONAGE_WHATSAPP_SANDBOX_NUMBER by default.
rawBodyMiddleware & verifyWebhookSignature: This is crucial for security. Vonage signs webhook requests with a JWT using your Signature Secret. verifySignature needs the raw, unparsed request body. We create middleware (rawBodyMiddleware) using express.raw to capture this raw body beforeexpress.json() parses it for specific webhook routes. The verifyWebhookSignature function extracts the JWT from the Authorization header and uses @vonage/jwt's verifySignature method along with the raw body and your secret to validate the request. Crucially, if the signature secret is missing or verification fails, the function now returns false, preventing insecure processing.
These routes use the rawBodyMiddleware first, thenexpress.json().
They must call verifyWebhookSignature to ensure the request is legitimate. Unauthorized requests are rejected with 401.
The /inbound handler logs the incoming message, extracts sender and content based on the channel, and optionally sends a reply using our helper functions.
The /status handler logs delivery status updates.
Both endpoints must respond with res.status(200).end() promptly to prevent Vonage from retrying the webhook.
API Endpoints (/api/send/sms, /api/send/whatsapp):
Simple POST endpoints are added to trigger sending messages externally. These routes use the standard express.json() middleware applied after the webhook routes.
They perform basic validation on the request body (to, message), including a simple digit check for the phone number, with notes on using better validation libraries. The error message clarifies the SDK's expectation for E.164 format.
They call the respective sendSms or sendWhatsApp functions.
Note: These endpoints lack proper authentication/authorization, which is essential for production (see Section 3). The @api comments are noted as usable with apidoc.
Server Start: The server starts listening on the configured PORT. We add checks/warnings on startup for missing optional environment variables.
3. Building a complete API layer
The example above includes basic API endpoints (/api/send/sms, /api/send/whatsapp). For a production system, you would enhance this layer significantly:
Authentication & Authorization: Protect your API endpoints. Common methods include:
API Keys: Issue unique keys to clients. Clients send the key in a header (e.g., X-API-Key). Your server validates the key against a stored list.
JWT (JSON Web Tokens): Implement a login flow where users/systems get a JWT upon successful authentication. They include the JWT in the Authorization: Bearer <token> header for subsequent requests. Libraries like passport with passport-jwt or passport-http-bearer (for API keys) can help.
OAuth2: For third-party integrations or more complex authorization scenarios.
Implementation Example (Conceptual - using a simple API key check middleware):
javascript
constVALID_API_KEYS=['your-super-secret-key-1','another-key'];// Store securely, e.g., in DB or vaultfunctionauthenticateApiKey(req, res, next){const apiKey = req.headers['x-api-key'];if(apiKey &&VALID_API_KEYS.includes(apiKey)){console.log('API Key authenticated.');next();// Key is valid, proceed}else{console.warn('API Key authentication failed.'); res.status(401).json({error:'Unauthorized: Invalid or missing API Key'});}}// Apply middleware to API routesapp.post('/api/send/sms', authenticateApiKey,async(req, res)=>{/* ... */});app.post('/api/send/whatsapp', authenticateApiKey,async(req, res)=>{/* ... */});
Request Validation: Sanitize and validate all incoming data rigorously.
Use libraries like express-validator or joi to define schemas for request bodies and query parameters.
Check data types, lengths, formats (e.g., ensure phone numbers adhere to E.164 using libraries like libphonenumber-js).
Example using express-validator:
bash
npminstall express-validator libphonenumber-js
javascript
const{ body, validationResult }=require('express-validator');const{ parsePhoneNumberFromString }=require('libphonenumber-js');app.post('/api/send/sms', authenticateApiKey,// Your auth middleware first// Validation rulesbody('to').custom(value =>{// Note: Vonage SDK expects E.164 without '+', but validation should check standard format.// Prepend '+' for robust parsing if necessary, then remove it for the SDK call.const numberToValidate = value.startsWith('+')? value :`+${value}`;const phoneNumber =parsePhoneNumberFromString(numberToValidate);if(!phoneNumber ||!phoneNumber.isValid()){thrownewError('Invalid E.164 phone number format required (e.g., +14155550100)');}// Ensure the original format (without '+') is used later if needed by SDK// req.body.to = value; // Keep original format passed in if it was valid without '+'returntrue;}),body('message').notEmpty().isLength({min:1,max:1600}).withMessage('Message is required and must be between 1 and 1600 characters'),async(req, res)=>{const errors =validationResult(req);if(!errors.isEmpty()){return res.status(400).json({errors: errors.array()});}// Validation passed, proceed with req.body// Ensure you use the correct format of 'to' for the sendSms functionconst{ to, message }= req.body;// ... call sendSms(to, message) ...});
This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to send SMS and WhatsApp messages via the Vonage Messages API. We will cover project setup, sending messages through different channels, handling incoming messages and status updates via webhooks, and implementing essential production considerations like security and error handling.
By the end of this tutorial, you will have a functional application capable of programmatically sending SMS and WhatsApp messages and receiving delivery status updates and inbound messages from users. This provides a foundation for building chatbots, notification systems, two-factor authentication flows, and other communication-driven features.
Project Overview and Goals
What We'll Build:
Problem Solved:
This application provides a unified way to interact with customers or users over two popular messaging channels – SMS and WhatsApp – directly from your Node.js backend. It abstracts the complexities of the Vonage API into reusable functions and provides a basic structure for handling asynchronous communication events.
Technologies Used:
@vonage/server-sdk
): Simplifies interaction with Vonage APIs, including authentication and sending messages.@vonage/messages
): Provides convenient classes for constructing messages for specific channels (likeWhatsAppText
).@vonage/jwt
): Used for verifying the signature of incoming webhook requests to ensure they originate from Vonage.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.ngrok
: A tool to expose local servers to the internet, necessary for testing webhooks during development.System Architecture:
Prerequisites:
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 your project, then navigate into it:
Initialize Node.js Project: Initialize the project using npm. The
-y
flag accepts default settings.This creates a
package.json
file.Install Dependencies: Install Express, the Vonage SDKs, and
dotenv
:express
: Web framework.@vonage/server-sdk
: Core Vonage SDK for authentication and API calls.@vonage/messages
: Specific classes for constructing message types (e.g.,WhatsAppText
).@vonage/jwt
: For webhook signature verification.dotenv
: For managing environment variables.Create Project Structure: Create a basic structure for your code:
src/server.js
: Our main application code..env
: Stores sensitive credentials and configuration..gitignore
: Prevents committing sensitive files (like.env
) andnode_modules
to version control.Configure
.gitignore
: Add the following lines to your.gitignore
file to avoid committing sensitive information and dependencies:Set up Vonage Application: You need a Vonage Application to group your numbers and configurations, and generate authentication credentials.
private.key
file. Save this file securely in the root of your project directory (e.g., alongsidepackage.json
). Crucially, ensureprivate.key
is listed in your.gitignore
file and is NEVER committed to version control. For production, load this key securely (e.g., from environment variables, secrets management, or a secure volume mount) rather than directly from the filesystem. The public key is stored by Vonage.ngrok
. Leave them blank for now or use temporary placeholders likehttp://localhost
.Configure SMS API Settings: Ensure your account uses the Messages API for SMS by default, as recommended by Vonage documentation for new integrations.
Set up WhatsApp Sandbox (for Testing): For testing WhatsApp without a full business setup, use the Vonage Sandbox.
Start
ngrok
: To receive webhooks from Vonage on your local machine, you need to expose your local server to the internet.ngrok
, specifying the port your Express server will listen on (we'll use 3000):ngrok
will display output including a Forwarding URL that looks likehttps://<random-string>.ngrok-free.app
(or similar, depending on your plan/version). Copy this HTTPS URL.Configure Webhook URLs in Vonage:
ngrok
HTTPS URL followed by/webhooks/inbound
into the Inbound URL field (e.g.,https://<random-string>.ngrok-free.app/webhooks/inbound
).ngrok
HTTPS URL followed by/webhooks/status
into the Status URL field (e.g.,https://<random-string>.ngrok-free.app/webhooks/status
)..../webhooks/inbound
and.../webhooks/status
) into the respective webhook fields on the Sandbox configuration page.Configure Environment Variables (
.env
): Open the.env
file you created and add the following variables, replacing the placeholder values with your actual credentials:VONAGE_API_KEY
&VONAGE_API_SECRET
: Found at the top of your Vonage API Dashboard.VONAGE_APPLICATION_ID
: The ID generated when you created the Vonage Application.VONAGE_PRIVATE_KEY
: The path to theprivate.key
file you downloaded and saved in your project root. Ensure this path is correct relative to where you run the node process.VONAGE_SMS_FROM_NUMBER
: Your purchased Vonage virtual number (without+
or leading00
).VONAGE_WHATSAPP_SANDBOX_NUMBER
: The specific number provided by the Vonage WhatsApp Sandbox. For production WhatsApp, this would be your registered WhatsApp Business number.VONAGE_API_SIGNATURE_SECRET
: Found in the Vonage API Dashboard under API Settings -> Webhook signature secret. Click ""Edit"" or ""Generate"" if needed.PORT
: The port your Express server will run on (must match thengrok
port).2. Implementing core functionality
Now, let's write the code in
src/server.js
to initialize the server, configure Vonage, send messages, and handle webhooks.Explanation:
dotenv.config()
loads variables from.env
.Vonage
client using credentials from environment variables. Checks ensure essential credentials (including signature secret) exist, exiting if not. We comment out theapiHost
override for the sandbox, preferring the default endpoint.sendSms
Function: TakestoNumber
andmessageText
, constructs the payload for the Messages API (channel: 'sms'
), and usesvonage.messages.send()
. Basic logging for success and failure is included.sendWhatsApp
Function: Similar tosendSms
, but uses theWhatsAppText
class from@vonage/messages
and specifieschannel: 'whatsapp'
. It uses theVONAGE_WHATSAPP_SANDBOX_NUMBER
by default.rawBodyMiddleware
&verifyWebhookSignature
: This is crucial for security. Vonage signs webhook requests with a JWT using your Signature Secret.verifySignature
needs the raw, unparsed request body. We create middleware (rawBodyMiddleware
) usingexpress.raw
to capture this raw body beforeexpress.json()
parses it for specific webhook routes. TheverifyWebhookSignature
function extracts the JWT from theAuthorization
header and uses@vonage/jwt
'sverifySignature
method along with the raw body and your secret to validate the request. Crucially, if the signature secret is missing or verification fails, the function now returnsfalse
, preventing insecure processing./webhooks/inbound
,/webhooks/status
):rawBodyMiddleware
first, thenexpress.json()
.verifyWebhookSignature
to ensure the request is legitimate. Unauthorized requests are rejected with401
./inbound
handler logs the incoming message, extracts sender and content based on the channel, and optionally sends a reply using our helper functions./status
handler logs delivery status updates.res.status(200).end()
promptly to prevent Vonage from retrying the webhook./api/send/sms
,/api/send/whatsapp
):express.json()
middleware applied after the webhook routes.to
,message
), including a simple digit check for the phone number, with notes on using better validation libraries. The error message clarifies the SDK's expectation for E.164 format.sendSms
orsendWhatsApp
functions.@api
comments are noted as usable withapidoc
.PORT
. We add checks/warnings on startup for missing optional environment variables.3. Building a complete API layer
The example above includes basic API endpoints (
/api/send/sms
,/api/send/whatsapp
). For a production system, you would enhance this layer significantly:Authentication & Authorization: Protect your API endpoints. Common methods include:
X-API-Key
). Your server validates the key against a stored list.Authorization: Bearer <token>
header for subsequent requests. Libraries likepassport
withpassport-jwt
orpassport-http-bearer
(for API keys) can help.Request Validation: Sanitize and validate all incoming data rigorously.
express-validator
orjoi
to define schemas for request bodies and query parameters.libphonenumber-js
).express-validator
: