This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to send and receive SMS messages via the Vonage Messages API. This forms a foundational component for applications like SMS marketing campaigns, notifications, or two-factor authentication.
We will build a simple application that can:
- Send an SMS message from a Vonage virtual number to a specified destination number.
- Receive incoming SMS messages sent to the Vonage virtual number via a webhook.
By the end of this guide, you will have a functional Node.js server capable of basic two-way SMS communication, ready for expansion into more complex features.
Project Overview and Goals
Goal: To create a robust Node.js service that leverages the Vonage Messages API for sending outgoing SMS and handling incoming SMS messages via webhooks, providing a reusable base for SMS-driven applications.
Problem Solved: This guide addresses the need for developers to integrate SMS capabilities into their Node.js applications reliably and efficiently, handling both sending and receiving workflows required for interactive communication or campaign responses.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications.
- Express: A minimal and flexible Node.js web application framework used here to create webhook endpoints.
- Vonage Messages API: A unified API for sending and receiving messages across multiple channels, including SMS. We specifically use this for its flexibility and features compared to older Vonage SMS APIs.
- @vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.
- ngrok: A tool to expose local development servers to the internet, necessary for testing webhooks.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
.
System Architecture:
+-----------------+ +------------------------+ +-------------+ +---------+
| User's Phone |<---->| Carrier Network |<---->| Vonage API |<---->| Carrier |<----> User's Phone
| (Recipient for | | | | (Messages | | Network | (Sender/Recipient)
| Outbound SMS, | | | | API) | | |
| Sender for | | | +-------------+ +---------+
| Inbound SMS) | | | ^ |
+-----------------+ +------------------------+ | | (Webhook POST: Inbound/Status)
(API Call: Send SMS) | v
+--------------------+
| Node.js/Express App|
| (Your Server) |
| - Sends API Req |
| - Receives Webhook|
+--------------------+
^ | (Local Dev via ngrok)
| v
+----------+
| ngrok |
+----------+
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. Download Node.js
- Vonage API Account: Create a free account to get API credentials and a virtual number. Sign up for Vonage
- Vonage Virtual Number: Purchase an SMS-capable number through the Vonage dashboard or CLI.
- ngrok: Installed and authenticated (a free account is sufficient). Download ngrok
- Vonage CLI (Optional but Recommended): For managing Vonage resources from your terminal. Install via npm:
npm install -g @vonage/cli
1. Setting Up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1. Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-app
cd vonage-sms-app
2. Initialize Node.js Project:
This creates a package.json
file to manage dependencies and project metadata.
npm init -y
3. Install Dependencies:
We need Express for the web server, the Vonage SDK to interact with the API, and dotenv
for managing environment variables securely.
npm install express @vonage/server-sdk dotenv
4. Create Project Files: Create the main files for our application logic and environment variables.
touch index.js server.js .env .gitignore
index.js
: Will contain the logic for sending SMS messages.server.js
: Will contain the Express server logic for receiving SMS messages via webhooks..env
: Will store sensitive credentials like Application ID, Private Key path/content, and phone numbers. Never commit this file to version control..gitignore
: Specifies intentionally untracked files that Git should ignore.
5. Configure .gitignore
:
Add node_modules
and .env
to your .gitignore
file to prevent committing them.
# .gitignore
node_modules/
.env
*.log
private.key # Or wherever you store your private key locally
6. Project Structure: Your project directory should now look like this:
vonage-sms-app/
├── node_modules/
├── .env
├── .gitignore
├── index.js
├── package-lock.json
├── package.json
└── server.js
This basic structure separates the sending logic (index.js
, often run as a one-off script) from the receiving server logic (server.js
, typically run as a long-running process) for clarity and modularity, although they could be combined in a single file for simpler applications. Using .env
ensures credentials are kept out of the codebase.
2. Implementing Core Functionality: Sending SMS
We'll start by implementing the code to send an SMS message using the Vonage Messages API.
1. Configure Environment Variables:
Open the .env
file and add placeholders for your Vonage Application credentials and numbers. You will obtain these values in Section 4 (Integrating with Vonage). Note that the Vonage Node SDK uses Application ID and Private Key for authentication with the Messages API.
# .env
# Vonage Application Credentials (Generated when creating a Vonage Application)
# These are typically used by the Vonage SDK for Messages API authentication.
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key # Path to your downloaded private key (for local dev)
# VONAGE_PRIVATE_KEY_CONTENT=""-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"" # Optional: For deployments, store key *content* here (ensure newlines are escaped if needed)
# Phone Numbers
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # The Vonage number you purchased
TO_NUMBER=RECIPIENT_PHONE_NUMBER # The number you want to send the SMS to (E.164 format, e.g., 14155550100)
2. Write Sending Logic (index.js
):
Edit index.js
to initialize the Vonage SDK and use the messages.send()
method.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');
const fs = require('fs'); // Needed to read the private key file if using path
// --- Configuration ---
// Ensure required environment variables are set
const requiredEnv = [
'VONAGE_APPLICATION_ID',
'VONAGE_NUMBER',
'TO_NUMBER'
];
// Either private key path or content must be provided
const privateKeyPath = process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH;
const privateKeyContent = process.env.VONAGE_PRIVATE_KEY_CONTENT;
if (!privateKeyPath && !privateKeyContent) {
console.error('Error: Environment variable VONAGE_APPLICATION_PRIVATE_KEY_PATH or VONAGE_PRIVATE_KEY_CONTENT must be set.');
process.exit(1);
}
if (privateKeyPath && !privateKeyContent && !fs.existsSync(privateKeyPath)) {
console.error(`Error: Private key file not found at path: ${privateKeyPath}`);
process.exit(1);
}
for (const variable of requiredEnv) {
if (!process.env[variable]) {
console.error(`Error: Environment variable ${variable} is not set.`);
process.exit(1); // Exit if essential config is missing
}
}
// Initialize Vonage SDK using Application ID and Private Key
// Prefers key content ENV var if present, otherwise uses path
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKeyContent || privateKeyPath,
});
const fromNumber = process.env.VONAGE_NUMBER;
const toNumber = process.env.TO_NUMBER;
const messageText = `Hello from Vonage and Node.js! Sent on ${new Date().toLocaleTimeString()}`;
// --- Send SMS Function ---
async function sendSms() {
console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`);
console.log(`Message: ""${messageText}""`);
try {
const resp = await vonage.messages.send(
new SMS({
to: toNumber,
from: fromNumber,
text: messageText,
}),
);
console.log('Message sent successfully!');
console.log('Message UUID:', resp.messageUuid); // Log the unique ID for tracking
} catch (err) {
console.error('Error sending SMS:');
// Log detailed error information if available
if (err.response && err.response.data) {
console.error('Status:', err.response.status);
console.error('Data:', JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
// Consider adding more robust error handling or retry logic here (See Section 5)
}
}
// --- Execute Sending ---
sendSms();
Explanation:
require('dotenv').config();
: Loads the variables from your.env
file intoprocess.env
.- Configuration Check: Basic validation ensures critical environment variables (Application ID, numbers, and either private key path or content) are present before proceeding. It also checks if the key file exists if the path is provided and content is not.
new Vonage({...})
: Initializes the SDK using your Application ID and the private key. The logic prioritizes using the key content fromVONAGE_PRIVATE_KEY_CONTENT
if set (common for deployments), otherwise it uses the file path fromVONAGE_APPLICATION_PRIVATE_KEY_PATH
. The Messages API primarily uses Application ID and Private Key for authentication when using the SDK.vonage.messages.send(...)
: This is the core function for sending messages.new SMS({...})
: We create an SMS message object specifying theto
,from
, andtext
parameters. The SDK handles structuring the request correctly for the Messages API.- Async/Await: We use
async/await
for cleaner handling of the asynchronous API call. - Response/Error Handling: The code logs the
messageUuid
on success. Thecatch
block logs detailed error information if the request fails, which is crucial for debugging.
3. Building an API Layer: Receiving SMS via Webhook
To receive SMS messages, Vonage needs a publicly accessible URL (a webhook) to send HTTP POST requests to whenever your virtual number receives a message. We'll use Express to create this endpoint.
1. Write Server Logic (server.js
):
Edit server.js
to set up an Express server listening for POST requests on a specific path.
// server.js
require('dotenv').config(); // Load environment variables
const express = require('express');
const { json, urlencoded } = express;
const app = express();
const PORT = process.env.PORT || 3000; // Use environment variable for port or default to 3000
// --- Middleware ---
// Parse incoming JSON requests (Vonage webhook format is JSON)
app.use(json());
// Parse URL-encoded requests (less common for Vonage webhooks, but good practice)
app.use(urlencoded({ extended: true }));
// --- Webhook Endpoint ---
// Define the route Vonage will POST to. Match this path in your Vonage Application settings.
const inboundWebhookPath = '/webhooks/inbound';
app.post(inboundWebhookPath, (req, res) => {
console.log('-----------------------------------------');
console.log(`Received Inbound SMS at ${new Date().toISOString()}`);
console.log('Request Body:');
console.log(JSON.stringify(req.body, null, 2)); // Log the entire incoming payload
// --- Process the Incoming Message ---
// Basic validation: Check for expected properties
if (req.body.from && req.body.to && req.body.text && req.body.message_uuid) {
console.log(`From: ${req.body.from.number || req.body.from.id}`); // Handle potential variations in 'from' structure
console.log(`To: ${req.body.to.number || req.body.to.id}`);
console.log(`Text: ${req.body.text}`);
console.log(`Message UUID: ${req.body.message_uuid}`);
// TODO: Add your business logic here (See Section 6 & 8)
// - Store the message in a database
// - Check for keywords (e.g., STOP, HELP)
// - Trigger automated replies
// - Enqueue tasks for processing
} else {
console.warn('Received unexpected webhook format:', req.body);
}
// --- Respond to Vonage ---
// IMPORTANT: Always send a 200 OK status back to Vonage quickly.
// Failure to do so will cause Vonage to retry sending the webhook,
// potentially leading to duplicate processing.
res.status(200).send('OK');
// Alternatively, use res.status(200).end(); if no body is needed.
console.log('Sent 200 OK response to Vonage.');
console.log('-----------------------------------------');
});
// --- Optional: Status Webhook Endpoint ---
// Vonage sends delivery receipts (DLRs) and other status updates here.
const statusWebhookPath = '/webhooks/status';
app.post(statusWebhookPath, (req, res) => {
console.log('=========================================');
console.log(`Received Status Update at ${new Date().toISOString()}`);
console.log('Request Body:');
console.log(JSON.stringify(req.body, null, 2));
// TODO: Process the status update (See Section 6 & 8)
// - Update message status in your database using message_uuid
// - Handle 'delivered', 'failed', 'rejected' statuses
res.status(200).send('OK');
console.log('Sent 200 OK response to Vonage.');
console.log('=========================================');
});
// --- Health Check Endpoint (Good Practice) ---
app.get('/health', (req, res) => {
res.status(200).send('Server is healthy');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Inbound SMS Webhook expected at: POST ${inboundWebhookPath}`);
console.log(`Status Webhook expected at: POST ${statusWebhookPath}`);
console.log(`Health check available at: GET /health`);
});
Explanation:
- Middleware:
express.json()
is essential for parsing the JSON payload sent by Vonage webhooks.express.urlencoded()
is included for general robustness. - Webhook Route (
/webhooks/inbound
): This specific path (POST
) listens for incoming messages. The path name can be customized, but it must match the Inbound URL configured in your Vonage application. - Logging: The entire request body (
req.body
) is logged. This is crucial for understanding the data structure Vonage sends. Specific fields like sender (from
), recipient (to
), message text (text
), andmessage_uuid
are logged individually. - Status Webhook Route (
/webhooks/status
): A separate endpoint to receive delivery receipts and status updates for outgoing messages you sent. This is vital for tracking message delivery. res.status(200).send('OK')
: This is critical. You must respond to Vonage with a 2xx status code (usually 200 OK) quickly to acknowledge receipt of the webhook. If Vonage doesn't receive this, it will assume the delivery failed and retry, leading to duplicate processing on your end.- Health Check: A simple
/health
endpoint is good practice for monitoring systems to check if the server is running. - Port: The server listens on the port specified by the
PORT
environment variable or defaults to 3000.
4. Integrating with Vonage: Configuration Steps
Now, let's configure your Vonage account and link it to the application code.
1. Purchase a Vonage Virtual Number:
- Log in to your Vonage API Dashboard.
- Navigate to
Numbers
>Buy numbers
. - Search for numbers with SMS capability in your desired country.
- Purchase a number.
- Copy the purchased number (in E.164 format, e.g.,
14155550100
) into theVONAGE_NUMBER
variable in your.env
file.
2. Create a Vonage Application: This application links your number, credentials, and webhook URLs together.
- Navigate to
Applications
>Create a new application
. - Give your application a meaningful name (e.g.,
Node SMS App - Dev
). - Generate Public/Private Key: Click the
Generate public and private key
button. This will automatically download theprivate.key
file. Save this file securely in your project directory (or another secure location for local development). UpdateVONAGE_APPLICATION_PRIVATE_KEY_PATH
in your.env
file to point to the correct path (e.g.,./private.key
if it's in the root). The public key is stored by Vonage. (For deployment, you'll likely use the key content viaVONAGE_PRIVATE_KEY_CONTENT
- see Section 12 if available, or adapt based on deployment strategy). - Capabilities: Toggle on the
Messages
capability. - Configure Webhooks:
- Inbound URL: Enter
YOUR_NGROK_HTTPS_URL/webhooks/inbound
(You'll getYOUR_NGROK_HTTPS_URL
in the next step). Set the method toPOST
. - Status URL: Enter
YOUR_NGROK_HTTPS_URL/webhooks/status
. Set the method toPOST
.
- Inbound URL: Enter
- Click
Generate new application
. - Copy Application ID: After creation, you'll see the Application ID. Copy this value into the
VONAGE_APPLICATION_ID
variable in your.env
file.
3. Link Your Number to the Application:
- Go back to
Numbers
>Your numbers
. - Find the number you purchased.
- Click the
Link
button (or edit icon) next to the number. - Select the application you just created (
Node SMS App - Dev
) from theForward to Application
dropdown under theMessages
capability. - Click
Save
.
4. Set Default SMS API (Crucial):
- Navigate to your main Dashboard Settings.
- Scroll down to
API settings
. - Find the
Default SMS Setting
section. - Select
Messages API
as the default handler for SMS. This ensures incoming messages use the Messages API format and webhooks. - Click
Save changes
.
5. Run ngrok: Now that the server code is ready, expose your local server to the internet using ngrok so Vonage can reach your webhooks. Run this in a separate terminal window.
# Make sure your Express server (server.js) is running or will run on port 3000
ngrok http 3000
- ngrok will display output including a
Forwarding
URL usinghttps
. It looks something likehttps://<random_string>.ngrok.io
. - Copy this HTTPS URL.
- Update Vonage Application: Go back to your Vonage Application settings (
Applications
> Your App Name > Edit). Paste the copied ngrok HTTPS URL into theInbound URL
andStatus URL
fields, making sure to append the correct paths (/webhooks/inbound
and/webhooks/status
).- Example Inbound URL:
https://a1b2c3d4e5f6.ngrok.io/webhooks/inbound
- Example Status URL:
https://a1b2c3d4e5f6.ngrok.io/webhooks/status
- Example Inbound URL:
- Save the changes to your Vonage Application.
Your local development environment is now configured to communicate with Vonage.
5. Implementing Error Handling, Logging, and Retry Mechanisms
Robust applications need proper error handling and logging.
Error Handling:
-
Sending (
index.js
): Thetry...catch
block already handles errors during thevonage.messages.send()
call. It logs detailed error information fromerr.response.data
if available. For production, consider:- Specific Error Codes: Check
err.response.status
or error codes withinerr.response.data
to handle specific issues differently (e.g., invalid number format, insufficient funds). - Retry Logic: For transient network errors or Vonage service issues (e.g., 5xx status codes), implement a retry mechanism with exponential backoff using libraries like
async-retry
orp-retry
. Caution: Do not retry errors like invalid number format (4xx errors) indefinitely.
First, install the library:
npm install async-retry
Then, you can implement retry logic like this:
// Example using async-retry in index.js const retry = require('async-retry'); // Assume vonage, SMS, fromNumber, toNumber, messageText are defined as before async function sendSmsWithRetry() { const smsDetails = new SMS({ to: toNumber, from: fromNumber, text: messageText, }); try { await retry( async bail => { console.log('Attempting to send SMS...'); try { const resp = await vonage.messages.send(smsDetails); console.log('Message sent successfully!', resp.messageUuid); // If successful, return the response or true to stop retry return resp; } catch (err) { console.error('Attempt failed:', err.message); // Don't retry on non-recoverable errors (e.g., 4xx client errors) if (err.response && err.response.status >= 400 && err.response.status < 500) { console.error('Non-retriable error:'_ JSON.stringify(err.response.data_ null_ 2)); bail(new Error('Non-retriable error encountered')); // Stop retrying return; // Exit async function } // For other errors (network_ 5xx)_ throw to trigger retry throw err; } }_ { retries: 3_ // Number of retries minTimeout: 1000_ // Initial delay ms factor: 2_ // Exponential backoff factor onRetry: (error_ attempt) => { console.warn(`Retrying sendSMS (Attempt ${attempt}). Error: ${error.message}`); } } ); } catch (error) { console.error('Failed to send SMS after multiple retries:', error.message); // Handle final failure (e.g., log to monitoring, notify admin) } } // Replace the simple sendSms() call in index.js with sendSmsWithRetry() // sendSmsWithRetry(); // Uncomment to use retry logic
- Specific Error Codes: Check
-
Receiving (
server.js
):- Wrap the core logic inside the webhook handlers (
app.post
) in atry...catch
block to handle unexpected errors during message processing (e.g., database errors). - Crucially, ensure the
res.status(200).send('OK')
happens outside the maintry...catch
or within afinally
block if you need to guarantee it, even if your internal processing fails. This prevents Vonage retries for issues unrelated to receiving the webhook itself.
// Example in server.js inbound webhook app.post(inboundWebhookPath, (req, res) => { try { console.log('-----------------------------------------'); console.log(`Received Inbound SMS at ${new Date().toISOString()}`); console.log('Request Body:'); console.log(JSON.stringify(req.body, null, 2)); // --- Start of Your Processing Logic --- if (req.body.from && req.body.to && req.body.text && req.body.message_uuid) { console.log(`Processing message from ${req.body.from.number}...`); // Your database interaction, keyword checking, etc. // Simulate potential error: // if (Math.random() > 0.8) throw new Error(""Simulated DB Error!""); } else { console.warn('Received unexpected webhook format'); } // --- End of Your Processing Logic --- } catch (error) { console.error('!!! Error processing inbound webhook:', error); // Log error to your tracking system (e.g., Sentry, Datadog) } finally { // Always acknowledge receipt to Vonage res.status(200).send('OK'); console.log('Sent 200 OK response to Vonage.'); console.log('-----------------------------------------'); } });
- Wrap the core logic inside the webhook handlers (
Logging:
-
The current
console.log
is suitable for development. -
For production, use a structured logging library like Pino or Winston:
- Structured Format (JSON): Easier for log aggregation tools (Datadog, Splunk, ELK stack) to parse.
- Log Levels: Differentiate between
info
,warn
,error
,debug
. - Log Destinations: Output to files, standard output, or external logging services.
Install Pino:
npm install pino pino-pretty # pino-pretty for dev readability
Integrate Pino:
// Example integration in server.js const pino = require('pino'); const logger = pino({ level: process.env.LOG_LEVEL || 'info', // Pretty print for development, JSON for production transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined, }); // Replace console.log with logger methods: // console.log(...) -> logger.info(...) // console.warn(...) -> logger.warn(...) // console.error(...) -> logger.error(...) // Example in inbound webhook app.post(inboundWebhookPath, (req, res) => { const logData = { webhook: 'inbound', body: req.body }; logger.info(logData, 'Received Inbound SMS'); try { // ... processing ... if (req.body.from && req.body.text && req.body.message_uuid) { logger.info(`Processing message from ${req.body.from.number}`); } else { logger.warn(logData, 'Unexpected webhook format'); } } catch (error) { logger.error({ err: error, webhook: 'inbound' }, 'Error processing inbound webhook'); } finally { res.status(200).send('OK'); logger.info({ webhook: 'inbound' }, 'Sent 200 OK response'); } }); // Similar replacements in status webhook and server start logging app.listen(PORT, () => { logger.info(`Server listening on port ${PORT}`); logger.info(`Inbound SMS Webhook expected at: POST ${inboundWebhookPath}`); logger.info(`Status Webhook expected at: POST ${statusWebhookPath}`); logger.info(`Health check available at: GET /health`); });
Retry Mechanisms:
- Vonage Webhook Retries: Vonage automatically retries sending webhooks if it doesn't receive a
2xx
response within a certain timeout (usually a few seconds). It uses an exponential backoff strategy. This handles temporary network issues between Vonage and your server. Your primary responsibility is to respond200 OK
promptly. - Application-Level Retries (Sending): As shown above with
async-retry
, implement this for sending SMS if you need resilience against temporary API failures on the Vonage side or network issues from your server to Vonage.
6. Creating a Database Schema and Data Layer (Conceptual)
While this basic guide doesn't implement a database, a real-world application (especially for campaigns) needs one.
Purpose:
- Log sent and received messages for audit trails and debugging.
- Track message delivery status using updates from the Status Webhook.
- Manage recipient lists and opt-out status for campaigns.
- Store campaign definitions and track progress.
Conceptual Schema (using Prisma as an example ORM):
// prisma/schema.prisma
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql"" // Or mysql, sqlite, etc.
url = env(""DATABASE_URL"")
}
model Message {
id String @id @default(cuid())
vonageMessageUuid String? @unique // From sending response or status/inbound webhook
direction Direction // INBOUND or OUTBOUND
fromNumber String
toNumber String
text String?
status String? // e.g., 'submitted', 'delivered', 'failed', 'read', 'received'
vonageStatus String? // Raw status code from Vonage (e.g., from status webhook)
errorCode String? // Error code if status is 'failed' or 'rejected' (from status webhook)
price Decimal? // Cost from Vonage status/usage data
currency String?
submittedAt DateTime @default(now()) // When our system processed/sent it
receivedAt DateTime? // When our system received it (for inbound)
vonageTimestamp DateTime? // Timestamp from Vonage webhook
lastUpdatedAt DateTime @updatedAt
@@index([vonageMessageUuid])
@@index([toNumber])
@@index([fromNumber])
@@index([status])
@@index([submittedAt])
}
enum Direction {
INBOUND
OUTBOUND
}
// Add models for Campaigns, Recipients, OptOuts etc. as needed
Implementation Steps (High-Level):
- Choose ORM/Driver: Select Prisma, Sequelize, TypeORM, or a database driver (
pg
,mysql2
). - Install Dependencies:
npm install @prisma/client
andnpm install -D prisma
. - Initialize Prisma:
npx prisma init
. - Define Schema: Create models in
prisma/schema.prisma
. - Set
DATABASE_URL
: Add your database connection string to.env
. - Run Migrations:
npx prisma migrate dev --name init
to create tables. - Generate Client:
npx prisma generate
. - Use Prisma Client: Import and use the client in your
index.js
andserver.js
to interact with the database.
// Example usage in server.js webhook handler
// Assumes logger is defined as in Section 5
// Assumes Prisma client is initialized
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient(); // Initialize Prisma Client
// Inside app.post('/webhooks/inbound', async (req, res) => { ... });
// Make the handler async to use await
app.post(inboundWebhookPath, async (req, res) => { // Note: async handler
const logData = { webhook: 'inbound', body: req.body };
// Use logger if integrated, otherwise console.log
// logger.info(logData, 'Received Inbound SMS');
console.log('Received Inbound SMS:', logData);
try {
if (req.body.from && req.body.text && req.body.to && req.body.message_uuid) {
const newMessage = await prisma.message.create({
data: {
vonageMessageUuid: req.body.message_uuid,
direction: 'INBOUND',
fromNumber: req.body.from.number,
toNumber: req.body.to.number,
text: req.body.text,
status: 'received', // Initial status for inbound
vonageTimestamp: req.body.timestamp ? new Date(req.body.timestamp) : new Date(), // Use Vonage timestamp if available
receivedAt: new Date(), // Record when our server got it
}
});
// logger.info({ messageId: newMessage.id }, 'Inbound message saved to DB');
console.log('Inbound message saved to DB:', newMessage.id);
// Check for STOP keyword (Example opt-out logic)
if (req.body.text.trim().toUpperCase() === 'STOP') {
// Add logic to mark fromNumber as opted-out in your recipient/opt-out table
// logger.warn({ number: req.body.from.number }, 'Received STOP keyword - Processing opt-out');
console.warn('Received STOP keyword - Processing opt-out for:', req.body.from.number);
// await prisma.optOut.upsert(...) or similar
}
// TODO: Add other keyword handling (HELP, etc.) or business logic
} else {
// logger.warn(logData, 'Received unexpected webhook format');
console.warn('Received unexpected webhook format:', logData);
}
} catch (error) {
// logger.error({ err: error, webhook: 'inbound' }, 'Error processing/saving inbound webhook');
console.error('Error processing/saving inbound webhook:', error);
// Even if DB fails, we should still acknowledge receipt to Vonage
} finally {
res.status(200).send('OK');
// logger.info({ webhook: 'inbound' }, 'Sent 200 OK response');
console.log('Sent 200 OK response to Vonage.');
}
});
// Similar logic needed for the status webhook:
// Find the message by vonageMessageUuid and update its status, errorCode, price etc.
// Example:
// app.post(statusWebhookPath, async (req, res) => {
// try {
// const { message_uuid, status, timestamp, error, price, currency } = req.body;
// if (message_uuid) {
// await prisma.message.update({
// where: { vonageMessageUuid: message_uuid },
// data: {
// status: status, // Update with the received status
// vonageStatus: status, // Store raw Vonage status if needed
// errorCode: error ? error.code : null, // Store error code if present
// price: price ? parseFloat(price) : null,
// currency: currency,
// vonageTimestamp: timestamp ? new Date(timestamp) : new Date(),
// lastUpdatedAt: new Date(), // Explicitly set or rely on @updatedAt
// }
// });
// console.log(`Updated status for message ${message_uuid} to ${status}`);
// } else {
// console.warn('Received status webhook without message_uuid:', req.body);
// }
// } catch (error) {
// console.error('Error processing status webhook:', error);
// } finally {
// res.status(200).send('OK');
// }
// });
Note: These code snippets assume you have fully set up Prisma (schema, database connection, migrations, client generation) as outlined in the high-level steps above. They will not run correctly without that setup.
7. Adding Security Features
Securing your application and webhook endpoints is crucial.
- Webhook Security:
- Signed Webhooks (JWT Verification - Recommended): The Vonage Messages API supports webhook signing using JSON Web Tokens (JWT) for verification. It is strongly recommended to implement JWT signature verification on your webhook endpoints to ensure requests genuinely originate from Vonage. Consult the Vonage Server SDK documentation or Messages API security guides for details on how to validate the incoming JWT, typically found in the
Authorization
header (e.g.,Bearer <token>
). The@vonage/server-sdk
may offer helper functions for this (check its documentation forverifySignature
or similar methods). - Obscurity: Use long, unguessable paths for your webhook URLs (less secure than signing, but better than simple paths like
/webhook
). - IP Whitelisting (Less Flexible): If Vonage publishes a list of egress IP addresses for webhooks, you could configure your firewall/load balancer to only accept requests from those IPs. This is often brittle as IPs can change without notice. Signature verification is generally preferred.
- Signed Webhooks (JWT Verification - Recommended): The Vonage Messages API supports webhook signing using JSON Web Tokens (JWT) for verification. It is strongly recommended to implement JWT signature verification on your webhook endpoints to ensure requests genuinely originate from Vonage. Consult the Vonage Server SDK documentation or Messages API security guides for details on how to validate the incoming JWT, typically found in the
- Input Validation:
- Always validate data received from webhooks before processing or storing it (e.g., check expected data types, lengths, formats). Libraries like
joi
orzod
can help define schemas for validation. - Sanitize any data that might be displayed back to users or used in database queries to prevent injection attacks (though ORMs often handle SQL injection).
- Always validate data received from webhooks before processing or storing it (e.g., check expected data types, lengths, formats). Libraries like
- Environment Variables: Keep sensitive information (API keys, private keys, database URLs) out of your codebase using environment variables and a
.env
file (added to.gitignore
). For production deployments, use secure environment variable management provided by your hosting platform (e.g., AWS Secrets Manager, Google Secret Manager, Heroku Config Vars). - Rate Limiting: Implement rate limiting on your webhook endpoints (using middleware like
express-rate-limit
) to prevent abuse or denial-of-service attacks. - HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this for local testing; production deployments must have valid SSL/TLS certificates).