Tracking the delivery status of your SMS messages is crucial for building reliable communication workflows. Knowing whether a message reached the recipient's handset – or why it failed – enables you to build smarter retry logic, provide accurate user feedback, and gain valuable insights into message deliverability.
This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to receive and process SMS delivery status updates from the Vonage Messages API via webhooks. We will cover project setup, Vonage configuration, implementing the webhook handler, sending test messages, handling errors, and preparing for production deployment.
Project Goal: To create a robust Node.js service that can:
- Send SMS messages using the Vonage Messages API.
- Receive real-time delivery status updates for those messages via a secure webhook endpoint.
- Log or store these status updates for analysis or further action.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework used to create the webhook endpoint.
- Vonage Messages API: A multi-channel API for sending and receiving messages, including SMS. We'll use it for sending SMS and receiving status updates.
@vonage/server-sdk
: The official Vonage Node.js SDK for interacting with the API.- ngrok: A tool to expose local servers to the internet, essential for testing webhooks during development.
dotenv
: A module to load environment variables from a.env
file.jsonwebtoken
: (Optional, for Security) A library to verify JWT signatures on incoming webhooks.
Prerequisites:
- A Vonage API account. Sign up here if you don't have one.
- Node.js and npm (or yarn) installed locally.
- A publicly accessible Vonage virtual phone number capable of sending SMS. You can purchase one from the Vonage API Dashboard.
- ngrok installed and authenticated. Download it here.
System Architecture
The flow involves two main interactions with the Vonage platform:
- Sending SMS: Your Node.js application uses the Vonage SDK to make an API request to Vonage, instructing it to send an SMS from your Vonage number to the recipient.
- Receiving Status Updates: When the status of the sent SMS changes (e.g., submitted, delivered, failed), the Vonage platform sends an HTTP POST request containing the status details to a pre-configured
Status URL
(your webhook endpoint). Your Express application listens at this URL, receives the data, and processes it.
1. Setting Up the Project
Let's create the project structure and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-status-app cd vonage-sms-status-app
-
Initialize Node.js Project: Initialize the project using npm or yarn. This creates a
package.json
file.npm init -y # or # yarn init -y
-
Install Dependencies: Install Express for the web server, the Vonage SDK, and
dotenv
for managing environment variables.npm install express @vonage/server-sdk dotenv # or # yarn add express @vonage/server-sdk dotenv
-
Create Project Structure: Set up a basic source directory and necessary files.
mkdir src touch src/index.js touch .env touch .gitignore
-
Configure
.gitignore
: Prevent sensitive files and build artifacts from being committed to version control. Add the following to your.gitignore
file:# .gitignore node_modules .env private.key npm-debug.log* yarn-debug.log* yarn-error.log*
-
Set Up Environment Variables (
.env
): Create a.env
file in the project root to store your Vonage credentials and configuration. Never commit this file to Git.# .env # Vonage API Credentials (Found in Vonage Dashboard > API Settings) # Note: API Key/Secret might be used for other Vonage APIs, but Messages API primarily uses App ID + Private Key VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials (Created in Vonage Dashboard > Your Applications) VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # IMPORTANT: Provide the *path* to your downloaded private key file. # This path is relative to the current working directory where the Node process starts. VONAGE_PRIVATE_KEY_PATH=./private.key # Your Vonage Virtual Number (Must be linked to the Application ID) VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Server Port PORT=3000 # Optional: For JWT Signature Verification (Store securely, NOT directly in .env for production) # Example structure - store the actual key securely, e.g., in secrets manager or env var # VONAGE_PUBLIC_KEY_STRING=""-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----""
VONAGE_API_KEY
,VONAGE_API_SECRET
: Found at the top of your Vonage API Dashboard. While present, the Messages API primarily uses JWT authentication (Application ID + Private Key).VONAGE_APPLICATION_ID
: Obtained after creating a Vonage Application (see next section).VONAGE_PRIVATE_KEY_PATH
: The file path to theprivate.key
file downloaded when creating the Vonage Application. Place this file in your project root or specify the correct path relative to where you run thenode
command. Ensure this file is readable by the Node.js process.VONAGE_NUMBER
: Your purchased Vonage virtual number, formatted with the country code (e.g.,14155550100
).PORT
: The port your Express server will listen on (defaulting to 3000).
2. Configuring Vonage
To send messages and receive status updates using the Messages API, you need to create a Vonage Application and configure it correctly.
-
Navigate to Vonage Applications: Log in to the Vonage API Dashboard and navigate to ""Your applications"" > ""Create a new application"".
-
Create the Application:
- Name: Give your application a descriptive name (e.g., ""SMS Status Webhook App"").
- Generate Public and Private Key: Click this button. Your browser will download a
private.key
file. Save this file securely in your project directory (or another location referenced viaVONAGE_PRIVATE_KEY_PATH
in your.env
file). Make sure this file is included in your.gitignore
. Vonage stores the public key associated with this application. - Application ID: Note the generated Application ID. Add it to your
.env
file asVONAGE_APPLICATION_ID
.
-
Enable Capabilities:
- Scroll down to the ""Capabilities"" section.
- Toggle Messages to enable it. This reveals fields for Inbound and Status URLs.
-
Configure Webhook URLs:
- Status URL: This is where Vonage will send delivery status updates. For now, enter a placeholder like
https://example.com/webhooks/status
. We will update this later with our ngrok URL during testing. Ensure the method is set to POST. - Inbound URL: If you also wanted to receive SMS messages (not just status updates), you would configure this URL. We can leave it blank for this guide or use a placeholder like
https://example.com/webhooks/inbound
. Ensure the method is set to POST.
- Status URL: This is where Vonage will send delivery status updates. For now, enter a placeholder like
-
Link Your Vonage Number:
- Scroll down to ""Link virtual numbers"".
- Find the Vonage number you want to use for sending SMS (the one specified in your
.env
file) and click ""Link"".
-
Save Changes: Click ""Save changes"" at the bottom of the page.
-
(Optional but Recommended) Ensure Messages API is Default: While the application settings usually suffice, you can double-check your account-level SMS settings. Go to ""Account"" > ""Settings"". Scroll to ""API settings"" > ""SMS settings"". Ensure the default API for sending SMS is set to ""Messages API"". This avoids potential conflicts if you previously used the older SMS API settings.
3. Implementing the Express Server (Webhook Handler)
Now, let's write the code for the Express server that will listen for incoming status webhooks from Vonage.
Edit src/index.js
:
// src/index.js
require('dotenv').config(); // Load environment variables from .env file (searches relative to CWD)
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs'); // Needed to read the private key file
// --- Basic Server Setup ---
const app = express();
// Vonage sends webhooks with application/json content type
app.use(express.json());
// Optional: If you need to handle URL-encoded data (not typical for Vonage webhooks)
app.use(express.urlencoded({ extended: true }));
const port = process.env.PORT || 3000;
// --- Vonage Client Initialization ---
// Read the private key from the file path specified in .env
// Ensure the path is correct relative to where the node process starts.
let privateKey;
try {
privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (err) {
console.error(""Error reading private key file:"", err);
process.exit(1); // Exit if key is essential and missing
}
// Initialize the Vonage SDK for the Messages API using Application ID and Private Key (JWT Auth)
// Note: Other Vonage APIs might use API Key/Secret authentication instead.
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY, // Optional for Messages API, but good practice to include
apiSecret: process.env.VONAGE_API_SECRET, // Optional for Messages API
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey // Use the key content read from the file
});
// --- Webhook Endpoint for Status Updates ---
// This path MUST match the path configured in the Vonage Application's Status URL
app.post('/webhooks/status', (req, res) => {
const statusData = req.body;
console.log('--- Vonage Status Webhook Received ---');
console.log('Timestamp:', statusData.timestamp);
console.log('Message UUID:', statusData.message_uuid);
console.log('Status:', statusData.status);
console.log('To:', statusData.to);
console.log('From:', statusData.from);
if (statusData.error) {
console.error('Error Code:', statusData.error.code);
console.error('Error Reason:', statusData.error.reason);
}
// Store or process the status update here (e.g., update a database)
// For this example, we just log it.
// See Section 7 for persistence ideas.
// IMPORTANT: Respond to Vonage with a 200 OK status code
// This acknowledges receipt of the webhook. Failure to do so
// will cause Vonage to retry sending the webhook.
res.status(200).send('OK');
// Alternatively: res.sendStatus(200);
});
// --- Basic Health Check Endpoint ---
app.get('/_health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
// --- Start the Server ---
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`Webhook endpoint available at /webhooks/status (POST)`);
});
Explanation:
require('dotenv').config()
: Loads variables from your.env
file intoprocess.env
. It looks for.env
starting from the current working directory (CWD) of the Node.js process.express()
: Creates an Express application instance.app.use(express.json())
: Adds middleware to parse incoming requests with JSON payloads (which Vonage uses for webhooks).- Reading
privateKey
: We usefs.readFileSync
to read the private key content from the path specified inVONAGE_PRIVATE_KEY_PATH
. Error handling is added in case the file doesn't exist or isn't readable. vonage = new Vonage(...)
: Initializes the Vonage SDK client. For the Messages API,applicationId
and the content of theprivateKey
are essential for JWT authentication. We pass the actual key content, not the file path, to the SDK.app.post('/webhooks/status', ...)
: Defines the route handler for POST requests to/webhooks/status
.- It logs the key fields from the incoming
req.body
(the webhook payload). Common fields includemessage_uuid
,status
(submitted
,delivered
,rejected
,undeliverable
,failed
),timestamp
,to
,from
, and an optionalerror
object if the status isfailed
orrejected
. - Crucially, it sends back a
200 OK
status usingres.status(200).send('OK');
. This tells Vonage you've successfully received the webhook. Without this, Vonage will retry sending the webhook according to its retry schedule, potentially causing duplicate processing.
- It logs the key fields from the incoming
app.get('/_health', ...)
: A simple endpoint to check if the server is running.app.listen(...)
: Starts the Express server on the specified port.
4. Sending an SMS to Trigger Status Updates
To test our webhook, we need to send an SMS message using the Vonage Messages API. The status updates for this message will then be sent to our webhook.
You can add a simple function to src/index.js
or create a separate script. Let's create a separate script for clarity.
Create src/send-test-sms.js
:
// src/send-test-sms.js
require('dotenv').config();
const { Vonage } = require('@vonage/server-sdk');
const fs = require('fs');
// Read the private key content
let privateKey;
try {
privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
} catch (err) {
console.error(""Error reading private key file:"", err);
process.exit(1);
}
// Initialize Vonage SDK (same as in index.js)
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey
});
// Replace with a valid recipient phone number
const RECIPIENT_NUMBER = ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER""; // e.g., 14155550101
async function sendSms() {
const fromNumber = process.env.VONAGE_NUMBER;
const toNumber = RECIPIENT_NUMBER;
const text = `Hello from Vonage! Testing status webhook. [${new Date().toLocaleTimeString()}]`;
if (!toNumber || toNumber === ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER"") { // Basic check
console.error('Error: Please replace RECIPIENT_NUMBER in src/send-test-sms.js');
process.exit(1);
}
if (!fromNumber) {
console.error('Error: VONAGE_NUMBER not found in .env file.');
process.exit(1);
}
console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`);
try {
const resp = await vonage.messages.send({
message_type: ""text"",
text: text,
to: toNumber,
from: fromNumber,
channel: ""sms""
});
console.log('SMS Submitted Successfully!');
console.log('Message UUID:', resp.message_uuid);
} catch (err) {
console.error('Error sending SMS:');
// Log specific Vonage error details if available
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
}
}
sendSms();
Explanation:
- Initializes the Vonage client similarly to
index.js
, including reading the private key file content. - Defines the
RECIPIENT_NUMBER
. Remember to replace the placeholder with a real phone number you can check. - Uses
vonage.messages.send()
to send the SMS.message_type: ""text""
: Specifies a plain text message.text
: The content of the SMS.to
: The recipient's phone number.from
: Your Vonage virtual number (from.env
).channel: ""sms""
: Specifies the SMS channel.
- Logs the
message_uuid
upon successful submission or logs the error details if the API call fails.
Before running: Edit src/send-test-sms.js
and replace ""REPLACE_WITH_RECIPIENT_PHONE_NUMBER""
with a valid E.164 formatted phone number.
5. Running Locally with ngrok
To allow Vonage's servers to reach your local development machine, you need to use ngrok.
-
Start ngrok: Open a new terminal window (keep the one for the server running separately). Run
ngrok
, telling it to forward traffic to the port your Express app is listening on (e.g., 3000).ngrok http 3000
-
Copy the ngrok URL: ngrok will display output similar to this:
ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000 Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
Copy the
https
Forwarding URL (e.g.,https://xxxxxxxx.ngrok.io
). Using HTTPS is strongly recommended. -
Update Vonage Status URL:
- Go back to your application settings in the Vonage API Dashboard (""Your applications"" > Select your app).
- Scroll to ""Capabilities"" > ""Messages"".
- Paste the copied
https
ngrok URL into the Status URL field, appending your webhook path:https://xxxxxxxx.ngrok.io/webhooks/status
. - Ensure the method is POST.
- Click Save changes.
Note: ngrok provides a temporary public URL suitable for development. For production, you will need to deploy your application to a server with a permanent public IP address or domain name and configure a valid SSL/TLS certificate. See Section 10.
6. Verification and Testing
Now, let's test the end-to-end flow.
-
Start the Express Server: In your first terminal window, run:
node src/index.js
You should see
Server listening at http://localhost:3000
. -
Send a Test SMS: In another terminal window (not the ngrok one), run the sending script:
node src/send-test-sms.js
You should see ""SMS Submitted Successfully!"" and a
message_uuid
. -
Observe Webhook Receipt:
- Watch the terminal where your Express server (
src/index.js
) is running. Within a few seconds to minutes (depending on carrier networks), you should start seeing logs like ""--- Vonage Status Webhook Received ---"" followed by the status details (submitted
, then potentiallydelivered
orfailed
). - Check the recipient phone number – it should receive the SMS message.
- Watch the terminal where your Express server (
-
Inspect with ngrok (Optional): Open the ngrok Web Interface URL (usually
http://127.0.0.1:4040
) in your browser. You can inspect the incoming POST requests to/webhooks/status
, view headers, the request body (payload), and your server's response (200 OK
). This is invaluable for debugging.
Manual Verification Checklist:
- Express server starts without errors (especially private key reading).
-
send-test-sms.js
executes and logs amessage_uuid
. - Recipient phone receives the SMS message.
- Express server logs show incoming requests to
/webhooks/status
. - Logged status progresses (e.g., from
submitted
todelivered
). - ngrok web interface shows POST requests to
/webhooks/status
receiving a200 OK
response.
7. Enhancements: Persistence and Data Storage
Logging status updates to the console is fine for testing, but in a production scenario, you'll want to store this information persistently, typically in a database.
Conceptual Database Schema:
A simple table to store message statuses might look like this (using PostgreSQL syntax):
CREATE TABLE message_statuses (
message_uuid VARCHAR(36) PRIMARY KEY, -- Vonage Message UUID
status VARCHAR(50) NOT NULL, -- e.g., 'submitted', 'delivered', 'failed'
recipient_number VARCHAR(20), -- To number
sender_number VARCHAR(20), -- From number (Your Vonage number)
-- Timestamp from Vonage webhook. Using TIMESTAMPTZ stores the timestamp
-- along with time zone information (typically converting to UTC for storage),
-- which is crucial for accurately recording event times from external systems
-- like Vonage, regardless of server or client time zones.
status_timestamp TIMESTAMPTZ NOT NULL,
error_code VARCHAR(50), -- Error code if status is 'failed'/'rejected'
error_reason TEXT, -- Error reason text
created_at TIMESTAMPTZ DEFAULT NOW(), -- When the record was first created (optional)
updated_at TIMESTAMPTZ DEFAULT NOW() -- When the record was last updated
);
Implementation Steps (Conceptual):
- Choose a Database: Select a database (e.g., PostgreSQL, MySQL, MongoDB).
- Install Database Driver/ORM: Add the appropriate Node.js driver or ORM (e.g.,
pg
for PostgreSQL,mysql2
for MySQL,mongoose
for MongoDB, or a higher-level ORM like Prisma or Sequelize).# Example for PostgreSQL with Prisma npm install @prisma/client npm install prisma --save-dev npx prisma init --datasource-provider postgresql # Define schema in prisma/schema.prisma based on the SQL above # npx prisma migrate dev --name init
- Update Webhook Handler: Modify the
/webhooks/status
handler insrc/index.js
to:- Initialize your database client/ORM.
- Inside the handler, use the incoming
statusData
(especiallymessage_uuid
) to find or create a record in your database table (anUPSERT
operation is often ideal). - Update the record with the latest
status
,status_timestamp
, and anyerror
details. Convert the ISO 8601 timestamp string from Vonage into a Date object for storage. - Implement appropriate error handling for database operations. Crucially, still ensure a
200 OK
is sent back to Vonage even if your database update fails, but log the database error internally for investigation. You might implement a separate retry mechanism for failed DB writes later.
// src/index.js - Conceptual Database Update in Webhook
// --- Assume Prisma Client is initialized as 'prisma' ---
// const { PrismaClient } = require('@prisma/client');
// const prisma = new PrismaClient();
app.post('/webhooks/status', async (req, res) => { // Make handler async
const statusData = req.body;
console.log('--- Vonage Status Webhook Received ---');
console.log(JSON.stringify(statusData, null, 2)); // Log full payload
try {
// Example: Using Prisma to update the database
await prisma.messageStatus.upsert({
where: { message_uuid: statusData.message_uuid },
update: {
status: statusData.status,
status_timestamp: new Date(statusData.timestamp), // Convert ISO string to Date object
error_code: statusData.error?.code,
error_reason: statusData.error?.reason,
updated_at: new Date()
},
create: {
message_uuid: statusData.message_uuid,
status: statusData.status,
recipient_number: statusData.to,
sender_number: statusData.from,
status_timestamp: new Date(statusData.timestamp), // Convert ISO string to Date object
error_code: statusData.error?.code,
error_reason: statusData.error?.reason,
// created_at and updated_at might have defaults in the DB schema
}
});
console.log(`Database updated for message: ${statusData.message_uuid}`);
} catch (dbError) {
console.error(`Database error processing status for ${statusData.message_uuid}:`, dbError);
// Decide on internal error handling/retry strategy for DB errors
// BUT STILL ACKNOWLEDGE THE WEBHOOK TO VONAGE
} finally {
// ALWAYS send 200 OK back to Vonage unless there's a catastrophic server failure
// preventing even this response.
res.status(200).send('OK');
}
});
8. Error Handling and Logging
Robust error handling and logging are essential for production.
- Webhook Handler:
- Wrap database/processing logic in
try...catch
blocks. - Log errors comprehensively, including the message UUID and the full error stack.
- Always return
200 OK
to Vonage unless your server is fundamentally unable to process any requests. Vonage has its own retry mechanism for non-2xx
responses; let it handle transient network issues. Focus your internal retries on downstream dependencies like your database if needed.
- Wrap database/processing logic in
- SMS Sending:
- Wrap
vonage.messages.send()
intry...catch
. - Log detailed errors, including potential response data from the Vonage API (
err.response.data
in Axios-based errors from the SDK).
- Wrap
- Structured Logging: Use a dedicated logging library like Winston or Pino for structured logging (e.g., JSON format). This makes logs easier to parse and analyze in production.
// Example using Winston (conceptual setup)
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // Control log level via env var
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json() // Use JSON format for structured logs
),
defaultMeta: { service: 'sms-status-service' },
transports: [
// In development, log to console with simple format
// In production, log to console (to be captured by container orchestrator)
// or configure file/remote transports
new winston.transports.Console({
format: process.env.NODE_ENV !== 'production'
? winston.format.simple()
: winston.format.json(), // Use simple format locally, JSON in prod
}),
],
});
// Replace console.log/console.error with logger.info/logger.error
// e.g., logger.info('Webhook received', { messageId: statusData.message_uuid, status: statusData.status });
// e.g., logger.error('Database update failed', { messageId: statusData.message_uuid, error: dbError.message, stack: dbError.stack });
9. Security Considerations
Protecting your webhook endpoint and credentials is vital.
-
Environment Variables: Never hardcode credentials. Use
.env
locally and secure environment variable management (like platform secrets or a secrets manager) in your deployment environment. Ensure.env
andprivate.key
are in.gitignore
. -
Private Key Handling: Treat your
private.key
file as highly sensitive. Ensure its permissions are restricted. In production deployments (especially containers), avoid copying the key file directly into the image; use secure methods like mounted volumes or secrets management tools (Docker secrets, Kubernetes secrets, cloud provider secrets managers). -
Webhook Signature Verification (Highly Recommended): The Vonage Messages API supports signing webhooks with JWT (JSON Web Tokens). This allows your application to verify that incoming requests genuinely originated from Vonage.
- Enable Signed Webhooks: In your Vonage Application settings under Messages > Webhook security, select ""Enable signed webhooks"" and choose ""JWT (recommended)"". Vonage will display the Public Key associated with your application.
- Store Public Key Securely: Obtain the Public Key string from the Vonage dashboard. Do not hardcode the public key directly in your source code. Store it securely, for example, as an environment variable or retrieve it from a secrets management service at runtime.
- Install Verification Library:
npm install jsonwebtoken # or # yarn add jsonwebtoken
- Implement Verification Middleware: Use the
jsonwebtoken
library and the Vonage public key to verify theAuthorization: Bearer <token>
header sent with each webhook.
// Conceptual JWT Verification Middleware const jwt = require('jsonwebtoken'); // Load the public key securely (e.g., from environment variable) // IMPORTANT: Avoid hardcoding this in production code. Load from secure source. const VONAGE_PUBLIC_KEY = process.env.VONAGE_PUBLIC_KEY_STRING; // Example format: ""-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"" if (!VONAGE_PUBLIC_KEY) { // Use your logger here console.warn(""VONAGE_PUBLIC_KEY environment variable not set. Skipping JWT verification. THIS IS INSECURE FOR PRODUCTION.""); } function verifyVonageSignature(req, res, next) { // Only verify if the public key was loaded if (!VONAGE_PUBLIC_KEY) { return next(); // Skip verification if key is missing (warning already logged) } const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { // Use your logger here console.warn('Missing or invalid Authorization header for JWT verification.'); return res.sendStatus(401); // Unauthorized } const token = authHeader.split(' ')[1]; try { // Verify the token using the application's public key // Ensure correct algorithm (RS256) which Vonage uses for Messages API JWT const decoded = jwt.verify(token, VONAGE_PUBLIC_KEY, { algorithms: ['RS256'] }); // Optional but Recommended: Check if decoded application_id matches your app ID if (decoded.application_id !== process.env.VONAGE_APPLICATION_ID) { // Use your logger here console.warn('JWT application_id mismatch', { jwtAppId: decoded.application_id, expectedAppId: process.env.VONAGE_APPLICATION_ID }); return res.sendStatus(401); // Or 403 Forbidden } // Attach decoded payload if needed for auditing, or just proceed req.vonage_jwt = decoded; // Example: make decoded token available // Use your logger here console.info('JWT signature verified successfully', { messageId: req.body?.message_uuid }); next(); // Signature is valid, proceed to the handler } catch (err) { // Use your logger here console.error('Invalid JWT signature', { error: err.message, tokenReceived: token ? 'yes' : 'no' }); return res.sendStatus(401); // Unauthorized - signature verification failed } } // Apply middleware *before* your route handler in src/index.js // Ensure logger is defined if using it within the middleware app.post('/webhooks/status', verifyVonageSignature, async (req, res) => { // ... existing handler logic ... });
-
HTTPS: Always use HTTPS for your webhook endpoint in production. Ensure your server has a valid SSL/TLS certificate configured.
-
Input Validation: Even with JWT verification, sanitize and validate expected fields within the webhook payload before using them (e.g., check data types, lengths, expected values for
status
). -
Rate Limiting: Implement rate limiting on your webhook endpoint using middleware like
express-rate-limit
to prevent abuse.