Tracking the delivery status of SMS messages is crucial for applications that rely on timely and reliable communication. Knowing whether a message reached the recipient's handset, failed, or was rejected allows you to build more robust workflows, provide better user feedback, and troubleshoot issues effectively.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via Plivo and receive real-time delivery status updates through webhooks (callbacks).
Project Goals:
Send an SMS message using the Plivo Node.js SDK.
Configure Plivo to send delivery status updates to a webhook endpoint.
Create an Express application to receive and process these delivery status callbacks.
Securely handle Plivo credentials and webhook requests.
Log delivery statuses for monitoring and debugging.
Technologies Used:
Node.js: A JavaScript runtime environment ideal for building scalable, event-driven applications like webhook handlers.
Express.js: A minimal and flexible Node.js web application framework used to create the webhook endpoint.
Plivo: A cloud communications platform providing SMS APIs and webhook capabilities for delivery reports.
Plivo Node.js SDK: Simplifies interaction with the Plivo API.
dotenv: A module to load environment variables from a .env file, keeping sensitive credentials out of source code.
ngrok (for development): A tool to expose local development servers to the internet, enabling Plivo to send webhooks to your local machine.
System Architecture:
The basic flow involves sending the message and receiving the status callback:
Send Request: Your Node.js application sends an SMS request to Plivo, specifying the recipient, message text, and your callback URL (e.g., /sms/delivery-report).
Acknowledge: Plivo acknowledges the request, providing a unique MessageUUID.
Deliver SMS: Plivo attempts to deliver the SMS to the recipient's handset.
Receive Status: The recipient's carrier network informs Plivo about the delivery status (e.g., delivered, failed).
Send Callback: Plivo sends an HTTP POST request to your specified callback URL, containing the MessageUUID, delivery Status, and other details.
Verify Signature: Your application verifies the X-Plivo-Signature-V2 header to ensure the request genuinely came from Plivo.
Respond OK: Your application responds with a 200 OK status to acknowledge receipt of the callback.
Log Status: Your application logs the delivery status associated with the MessageUUID for monitoring or further processing.
Prerequisites:
Node.js and npm (or yarn): Installed on your system. (Download: https://nodejs.org/)
Let's create the project directory, initialize it, and install the necessary dependencies.
Create Project Directory:
Open your terminal or command prompt and create a new directory for your project.
Bash
mkdir plivo-sms-callbacks
cd plivo-sms-callbacks
Initialize Node.js Project:
This creates a package.json file to manage your project's dependencies and scripts.
Bash
npm init -y
(The -y flag accepts the default settings)
Install Dependencies:
We need Express for the web server, the Plivo SDK, and dotenv for managing environment variables.
Bash
npm install express plivo-node dotenv
Set Up Environment Variables:
Create a file named .env in the root of your project directory. This file will store your sensitive Plivo credentials. Never commit this file to version control.
Text
# .env
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
PLIVO_SENDER_ID=YOUR_PLIVO_PHONE_NUMBER_OR_SENDER_ID
# Optional: Define the port for your Express server
PORT=3000
# Optional: Base URL for your webhook endpoint (useful for ngrok)
BASE_URL=http://localhost:3000
PLIVO_AUTH_ID & PLIVO_AUTH_TOKEN: Find these on your Plivo Console dashboard (https://console.plivo.com/dashboard/). Copy the Auth ID and Auth Token.
PLIVO_SENDER_ID: This is the Plivo phone number (in E.164 format, e.g., +14155551212) or Alphanumeric Sender ID you'll use to send messages. You must own this number/ID in your Plivo account.
PORT: The local port your Express server will listen on.
BASE_URL: The public base URL where your application will be reachable. For local development with ngrok, you'll update this later.
Create .gitignore:
Ensure your .env file and node_modules directory are not committed to Git. Create a file named .gitignore:
We'll create a simple script to send an SMS message using the Plivo SDK. Crucially, we will specify the url parameter in the messages.create call. This URL tells Plivo where to send the delivery status updates.
Create sendSms.js:
Create a file named sendSms.js in your project root.
JavaScript
// sendSms.jsrequire('dotenv').config(); // Load environment variables from .env fileconst plivo = require('plivo');
// Retrieve credentials and sender ID from environment variablesconst authId = process.env.PLIVO_AUTH_ID;
const authToken = process.env.PLIVO_AUTH_TOKEN;
const senderId = process.env.PLIVO_SENDER_ID;
const baseUrl = process.env.BASE_URL; // Base URL for callbacks// Validate that environment variables are setif (!authId || !authToken || !senderId || !baseUrl) {
console.error(
'Error: Plivo credentials, sender ID, or base URL not found in .env file.'
);
process.exit(1); // Exit if configuration is missing
}
// Create Plivo client instanceconst client = new plivo.Client(authId, authToken);
// --- Function to Send SMS ---asyncfunctionsendSms(destinationNumber, messageText) {
// Construct the full callback URL// This tells Plivo where to POST the delivery statusconst deliveryReportUrl = `${baseUrl}/sms/delivery-report`;
console.log(`Sending SMS to ${destinationNumber}...`);
console.log(`Configuring delivery report URL: ${deliveryReportUrl}`);
try {
const response = await client.messages.create(
senderId, // src: Sender ID or Plivo number
destinationNumber, // dst: Recipient number in E.164 format
messageText, // text: The message content
{
url: deliveryReportUrl, // The URL for delivery status callbacksmethod: 'POST', // Method Plivo should use to call the URL (POST is recommended)
}
);
console.log('SMS Sent Successfully!');
console.log('API Response:', response);
// The response contains the Message UUID(s) which link to the delivery reportconsole.log('Message UUID(s):', response.messageUuid);
} catch (error) {
console.error('Error sending SMS:', error);
}
}
// --- Example Usage ---// Get destination number and message from command line arguments// Usage: node sendSms.js <destination_number_E.164> ""Your message here""const args = process.argv.slice(2); // Remove 'node' and script nameconst destination = args[0];
const text = args[1];
if (!destination || !text) {
console.log(
'Usage: node sendSms.js <destination_number_E.164> ""<message_text>""'
);
console.log(
'Example: node sendSms.js +12025551234 ""Hello from Plivo!""'
);
process.exit(1);
}
// Validate E.164 format (basic check)if (!/^\+[1-9]\d{1,14}$/.test(destination)) {
console.error('Error: Destination number must be in E.164 format (e.g., +12025551234)');
process.exit(1);
}
sendSms(destination, text);
Explanation:
require('dotenv').config();: Loads variables from your .env file into process.env.
Credentials Validation: Checks if necessary environment variables are loaded.
new plivo.Client(): Initializes the Plivo client with your credentials.
sendSms Function:
Takes the destination number and message text as input.
Constructs deliveryReportUrl using the BASE_URL from .env and a specific path (/sms/delivery-report). This is the endpoint we will create in our Express app.
Calls client.messages.create() with:
src: Your Plivo sender ID/number.
dst: The recipient's phone number (must be in E.164 format).
text: The content of the SMS.
url: The crucial parameter pointing to your webhook endpoint.
method: Specifies that Plivo should use the POST HTTP method to send data to your url. POST is generally preferred for sending data.
Command Line Arguments: The script is set up to take the destination number and message text from the command line for easy testing.
E.164 Validation: A simple regex check ensures the destination number starts with + and digits.
Testing (Initial):
You can try running this now, but the callback will fail because we haven't set up the server to receive it yet.
(Replace <your_recipient_phone_number> with a valid phone number in E.164 format, preferably one you can check. If using a trial account, this must be a number verified in your Plivo Sandbox).
You should see the ""SMS Sent Successfully!"" message and the Message UUID. Check the Plivo logs (https://console.plivo.com/logs/message/) – you'll likely see the message initially as ""queued"" or ""sent"", and later an attempt (and failure) by Plivo to POST to your (non-existent) callback URL.
3. Building the API Layer: Receiving Callbacks
Now, let's create the Express server and the specific endpoint (/sms/delivery-report) to receive the delivery status callbacks from Plivo.
Create server.js:
Create a file named server.js in your project root.
JavaScript
// server.jsrequire('dotenv').config();
const express = require('express');
const crypto = require('crypto'); // Node.js crypto module for signature verificationconst app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000// --- Middleware ---// 1. Body Parsing Middleware:// Plivo sends callbacks typically as application/x-www-form-urlencoded// Use express.urlencoded to parse this data into req.body// Important: Use { extended: true } for richer objects.// Signature V2 does not require capturing the raw body.
app.use(express.urlencoded({ extended: true }));
// 2. Plivo Signature Verification Middleware (CRITICAL FOR SECURITY)constverifyPlivoSignature = (req, res, next) => {
const plivoSignature = req.header('X-Plivo-Signature-V2');
const nonce = req.header('X-Plivo-Signature-V2-Nonce');
const authToken = process.env.PLIVO_AUTH_TOKEN;
if (!plivoSignature || !nonce || !authToken) {
console.warn('Missing Plivo signature headers or auth token.');
// Don't reveal specifics about missing auth token in responsereturn res.status(400).send('Missing signature headers');
}
// Construct the full URL Plivo used to call your webhook// Note: Use req.protocol, req.hostname, and req.originalUrl// In complex proxy setups or non-standard ports, Plivo might sign the URL// including the port. req.hostname doesn't include the port. If signature// verification fails unexpectedly, try using req.get('host') instead of req.hostname,// or log the exact URL Plivo calls (visible in Plivo Debug Logs or ngrok inspector)// and ensure your reconstructed URL matches precisely.// Use req.get('host') for better compatibility behind proxies/non-standard ports.const currentHost = req.get('host'); // Includes hostname and potentially portconst fullUrl = `${req.protocol}://${currentHost}${req.originalUrl}`;
// Create the base stringconst baseString = `${fullUrl}${nonce}`;
// Create the HMAC-SHA256 signatureconst expectedSignature = crypto
.createHmac('sha256', authToken)
.update(baseString)
.digest('base64');
// Compare signatures using timingSafeEqual to prevent timing attackstry {
// Ensure both buffers are valid before comparingconst signatureBuffer = Buffer.from(plivoSignature);
const expectedBuffer = Buffer.from(expectedSignature);
if (signatureBuffer.length !== expectedBuffer.length) {
console.error('Invalid Plivo signature length.');
return res.status(403).send('Invalid signature');
}
if (crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
console.log('Plivo signature verified successfully.');
next(); // Signature matches, proceed to the route handler
} else {
console.error('Invalid Plivo signature.');
res.status(403).send('Invalid signature'); // Forbidden
}
} catch (error) {
console.error('Error during signature comparison:', error);
res.status(400).send('Invalid signature format'); // Bad request if buffers are bad
}
};
// --- Routes ---// 1. Health Check Route
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// 2. SMS Delivery Report Route// Apply the signature verification middleware ONLY to this route
app.post('/sms/delivery-report', verifyPlivoSignature, (req, res) => {
// The request body now contains the parsed form dataconst deliveryData = req.body;
console.log('--- Received Plivo Delivery Report ---');
console.log('Timestamp:', newDate().toISOString());
console.log('Status:', deliveryData.Status);
console.log('Message UUID:', deliveryData.MessageUUID);
console.log('From:', deliveryData.From); // Sender ID usedconsole.log('To:', deliveryData.To); // Recipient number// Add more fields as needed (e.g., ErrorCode, MessageTime)// See Plivo docs for all possible parameters:// https://www.plivo.com/docs/messaging/concepts/message-delivery-reports/#delivery-report-parametersconsole.log('Full Payload:', JSON.stringify(deliveryData, null, 2)); // Log full payload nicelyconsole.log('------------------------------------');
// --- Add your business logic here ---// - Update your database with the status for the MessageUUID// - Trigger notifications or further actions based on the status// - Example: If Status is 'failed' or 'undelivered', enqueue a retry or notify support.// Acknowledge receipt to Plivo// You MUST send a 2xx status code (e.g., 200 OK) quickly,// otherwise Plivo may consider the callback failed and retry.
res.status(200).send('Delivery report received.');
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
console.log(`Webhook endpoint ready at /sms/delivery-report (POST)`);
console.log(`Health check endpoint ready at /health (GET)`);
});
Explanation:
Middleware:
express.urlencoded({ extended: true }): Parses incoming requests with Content-Type: application/x-www-form-urlencoded. The extended: true option allows for rich objects and arrays to be encoded into the URL-encoded format. Plivo V2 signature verification does not require the raw request body.
verifyPlivoSignature: This custom middleware is essential for security.
It retrieves the X-Plivo-Signature-V2 and X-Plivo-Signature-V2-Nonce headers sent by Plivo, along with the PLIVO_AUTH_TOKEN from environment variables.
URL Reconstruction: It reconstructs the full URL Plivo called using req.protocol, req.get('host') (which includes hostname and port, making it more robust behind proxies), and req.originalUrl.
It creates the baseString by concatenating the full URL and the nonce.
It calculates the expected signature using crypto.createHmac('sha256', ...) with your PLIVO_AUTH_TOKEN and the baseString.
It compares the calculated signature with the one received from Plivo using crypto.timingSafeEqual (important to prevent timing attacks). A try...catch block handles potential errors during comparison (e.g., invalid base64), and an explicit length check is added before timingSafeEqual.
If the signatures match, it calls next() to proceed to the route handler. Otherwise, it sends a 403 Forbidden or 400 Bad Request response.
Routes:
/health (GET): A simple endpoint to check if the server is running.
/sms/delivery-report (POST):
This route specifically handles incoming POST requests from Plivo for delivery reports.
It applies the verifyPlivoSignature middleware before the main handler.
The handler logs the received data (req.body), which includes Status, MessageUUID, From, To, and potentially ErrorCode.
Important: It sends back a 200 OK status code promptly to acknowledge receipt. Failure to do so might cause Plivo to retry the callback.
This is where you would add your application-specific logic (e.g., updating a database).
Server Start: Starts the Express server, listening on the specified port.
4. Integrating with Plivo & Local Testing (ngrok)
To receive callbacks on your local machine during development, you need a way to expose your local server to the public internet. ngrok is perfect for this.
Start Your Local Server:
Open a terminal in your project directory and run:
Bash
node server.js
You should see Server listening on port 3000....
Start ngrok:
Open a second terminal window (leave the server running) and start ngrok, telling it to forward to the port your server is running on (e.g., 3000).
Bash
ngrok http 3000
Get Your Public ngrok URL:ngrok will display output similar to this:
Code
Session Status online
Account Your Name (Plan: Free)
Version x.x.x
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://<random_string>.ngrok.io -> http://localhost:3000
Forwarding https://<random_string>.ngrok.io -> http://localhost:3000
Copy the https:// forwarding URL (e.g., https://<random_string>.ngrok.io). This is your temporary public URL. Using HTTPS is strongly recommended.
Update .env:
Go back to your .env file and update the BASE_URL with your public ngrok HTTPS URL.
Text
# .env
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
PLIVO_SENDER_ID=YOUR_PLIVO_PHONE_NUMBER_OR_SENDER_ID
PORT=3000
BASE_URL=https://<random_string>.ngrok.io # <-- UPDATE THIS WITH YOUR NGROK HTTPS URL
Restart Your Server (Important):
Stop your Node.js server (Ctrl+C in the first terminal) and restart it (node server.js) after changing the .env file to ensure it picks up the new BASE_URL.
Send Another Test SMS:
Now, run the sendSms.js script again with a valid recipient number.
Bash
node sendSms.js <your_recipient_phone_number> ""Test message with callback""
Verify Callback:
Node Server Logs: Watch the terminal where node server.js is running. Within a few seconds to minutes (depending on the carrier), you should see the log output from the /sms/delivery-report route, including ""Plivo signature verified successfully."", the Status (e.g., delivered, sent, failed, undelivered), and MessageUUID.
ngrok Logs: The terminal running ngrok will show incoming POST /sms/delivery-report requests with a 200 OK response. You can also inspect requests in detail via the ngrok web interface (usually http://127.0.0.1:4040).
Plivo Logs: Check the Plivo Message Logs again (https://console.plivo.com/logs/message/). You should see the final delivery status for your message, and the log entry for the callback attempt should now show a 200 OK status.
The server.js handles signature verification errors (400/403). Other potential errors (like database issues if you add them) should be caught within the route handler, logged, but still ideally return a 2xx code to Plivo unless the request itself was malformed (4xx). Avoid 5xx responses if possible, as Plivo might retry aggressively.
Logging:
We are using console.log for simplicity. For production, use a more robust logging library like winston or pino.
Structure your logs (e.g., JSON format) for easier parsing and analysis by log management systems (like Datadog, Splunk, ELK stack).
Retry Mechanisms:
Sending: If sendSms.js fails due to a temporary network issue or a Plivo server error (5xx), you might implement a simple retry logic (e.g., using async-retry) with exponential backoff.
Receiving Callbacks: Plivo handles retrying callbacks if your endpoint doesn't respond with 2xx within a timeout period (typically ~5 seconds). Your responsibility is to ensure your endpoint is reliable, responds quickly (under the timeout), and handles requests idempotently (if necessary, though usually not required for simple status logging based on unique MessageUUID).
6. Database Schema and Data Layer (Conceptual)
While this guide doesn't implement a database, here's how you would typically integrate it:
Schema:
You'd likely have a table to store message details and their status updates.
SQL
-- Example PostgreSQL SchemaCREATE TABLE sms_messages (
message_uuid UUID PRIMARY KEY, -- Plivo's MessageUUID
sender_id VARCHAR(50) NOT NULL,
recipient_number VARCHAR(20) NOT NULL,
message_text TEXT,
initial_api_response JSONB, -- Store the response from messages.create
status VARCHAR(50) DEFAULT'queued', -- e.g., queued, sent, delivered, failed, undelivered
status_timestamp TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP,
error_code VARCHAR(10),
created_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP
);
-- Index for efficient status lookupsCREATE INDEX idx_sms_messages_status ON sms_messages(status);
CREATE INDEX idx_sms_messages_recipient ON sms_messages(recipient_number);
Integration:
In sendSms.js: After successfully calling client.messages.create, insert a new record into sms_messages with the messageUuid, recipient, sender, text, initial response, and set status to 'queued' or 'sent' based on the API response.
In server.js (/sms/delivery-report):
Extract the MessageUUID and Status (and ErrorCode if present) from req.body.
Find the corresponding record in sms_messages using the MessageUUID.
Update the status, status_timestamp, and error_code for that record.
Use a database transaction if you need to perform multiple updates atomically.
Data Layer: Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js) to interact with your database safely and efficiently.
7. Security Features
Webhook Signature Verification:Implemented and crucial. This prevents attackers from sending fake callbacks to your endpoint. Always use the latest signature version offered by Plivo (V2 currently).
Environment Variables: Keep PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and any other secrets out of your code and .env files out of version control.
HTTPS: Always use HTTPS for your callback URL (ngrok provides this, and production deployments must use TLS/SSL). This encrypts the data in transit.
Input Validation (Sending): If the destination number or message text in sendSms.js comes from user input, validate and sanitize it thoroughly to prevent injection attacks or abuse. Ensure numbers are in E.164 format.
Rate Limiting: Apply rate limiting to your callback endpoint (/sms/delivery-report) using middleware like express-rate-limit to prevent abuse or accidental denial-of-service if Plivo retries excessively for some reason.
JavaScript
// Example in server.js (install: npm install express-rate-limit)const rateLimit = require('express-rate-limit');
const callbackLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 500, // Limit each IP to 500 requests per windowMs (adjust as needed)message: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply to the callback route *before* signature verification
app.post('/sms/delivery-report', callbackLimiter, verifyPlivoSignature, (req, res) => {
// ... handler logic
});
Firewall: Configure server firewalls to only allow traffic on necessary ports (e.g., 443 for HTTPS). You might restrict access to the callback endpoint to only known Plivo IP addresses if feasible and documented by Plivo, but signature verification is generally the preferred and more flexible approach.
8. Handling Special Cases
E.164 Format: Plivo requires destination numbers (dst) to be in E.164 format (e.g., +14155551234). Ensure any user input is normalized to this format before sending.
Multiple Message UUIDs: If you send a long SMS that gets split into multiple segments, the messages.create response might contain multiple messageUuids. The delivery report callback will typically be sent per segment, each with its corresponding MessageUUID. Your database schema and logic should handle this many-to-one relationship if segment-level tracking is needed. Often, tracking the status of the first UUID is sufficient to know the overall message delivery attempt status.
Status Meanings: Understand the different Status values (queued, sent, delivered, failed, undelivered) and any associated ErrorCode values. See Plivo documentation for details. Build your application logic accordingly.
Callback Timeouts: Ensure your /sms/delivery-report endpoint responds quickly (within Plivo's timeout, typically ~5 seconds, ideally under 1-2 seconds) with a 200 OK. Long-running tasks should be offloaded to a background job queue (e.g., BullMQ, Kue) triggered by the callback, rather than being executed synchronously within the request handler.
9. Performance Optimizations
Lightweight Endpoint: Keep the /sms/delivery-report handler fast. Do minimal work: verify signature, parse data, acknowledge receipt (200 OK), and potentially enqueue a background job for heavier processing (like database updates, notifications).
Asynchronous Operations: Use async/await for I/O operations (like database calls, though ideally offloaded).
Database Indexing: Ensure message_uuid is indexed (ideally primary key) for fast lookups when updating status. Index other commonly queried fields like status or recipient_number.
Load Testing: Use tools like k6, artillery, or autocannon to test how many concurrent callbacks your server can handle.
Node.js Clustering: For high throughput, run your Node.js application using the built-in cluster module or a process manager like pm2 in cluster mode to utilize multiple CPU cores.
10. Monitoring, Observability, and Analytics
Health Checks: The /health endpoint provides a basic check. Production monitoring systems (like Prometheus/Grafana, Datadog, New Relic) should poll this endpoint.
Application Performance Monitoring (APM): Tools like Datadog APM, New Relic, or Dynatrace can provide deep insights into request latency, error rates, resource usage, and trace requests through your system (including callbacks).
Log Aggregation: Centralize logs from your application instances using tools like the ELK Stack (Elasticsearch, Logstash, Kibana), Graylog, Splunk, or cloud provider services (AWS CloudWatch Logs, Google Cloud Logging).
Metrics: Track key metrics:
Number of callbacks received per status (delivered, failed, etc.).
Callback processing latency (time taken by the /sms/delivery-report handler).
Error rate of the callback endpoint.
Rate of signature verification failures.
Alerting: Set up alerts based on metrics and logs:
High rate of failed/undelivered statuses.
High callback endpoint error rate (>1%).
High callback processing latency.
Significant number of signature verification failures.
Health check failures.
11. Troubleshooting and Caveats
Incorrect Auth ID/Token: Leads to 401 Unauthorized errors when sending SMS or signature verification failures. Double-check credentials in .env and the Plivo console.
Invalid src (Sender ID): Ensure the PLIVO_SENDER_ID is a valid Plivo number (in E.164) or an approved Alphanumeric Sender ID associated with your account. Using an unowned number results in errors.
Invalid dst (Recipient Number): Must be E.164 format. Sending to incorrectly formatted numbers will fail. Trial accounts can only send to numbers verified in the Sandbox.
Callback URL Issues:
Not Publicly Accessible: Plivo cannot reach localhost. Use ngrok for local testing or deploy to a public server.
Incorrect URL in sendSms: Ensure the url parameter matches the endpoint route in server.js and uses the correct BASE_URL. Check for typos.
HTTP vs HTTPS: Use HTTPS for ngrok and production URLs. Plivo may not send callbacks to HTTP URLs.
Server Not Running: Ensure node server.js is running when expecting callbacks.
Firewall Blocking: Server firewalls might block incoming requests from Plivo's IP range.
Callback Not Responding 200 OK: If your endpoint errors out (5xx) or times out, Plivo won't know you received the data and will retry, potentially causing duplicate processing if your handler isn't idempotent. Log errors but try to return 200 OK.
Signature Verification Failure:
Check PLIVO_AUTH_TOKEN is correct in your .env file and matches the token in the Plivo Console.
Ensure the URL reconstruction (e.g., ${req.protocol}://${req.get('host')}${req.originalUrl}) exactly matches what Plivo used. Check ngrok inspector or server logs for the exact URL Plivo called. Proxies or non-standard ports can sometimes alter hostname/protocol headers. Using req.get('host') is generally more robust than req.hostname.
Verify the correct nonce and signature headers (X-Plivo-Signature-V2, X-Plivo-Signature-V2-Nonce) are being read and used.
Body Parsing Issues: Ensure express.urlencoded({ extended: true }) middleware is used before your route handler if the Content-Type is application/x-www-form-urlencoded (which is typical for Plivo callbacks). If Plivo were sending JSON (application/json), you would use express.json() instead.
Frequently asked questions
Track SMS delivery status using Plivo's webhook feature, which sends real-time updates to your application. Configure a callback URL in your Plivo settings and your application will receive delivery reports via HTTP POST requests to this URL. These reports contain details like message status, UUID, and error codes.
A Plivo Message UUID is a unique identifier assigned to each SMS message sent through the Plivo platform. This UUID is crucial for tracking the delivery status of individual messages and associating them with specific delivery reports received via webhooks.
Plivo uses webhooks (callbacks) for delivery reports to provide real-time status updates as they happen. Instead of your application constantly polling Plivo for updates, Plivo pushes the information to your application as soon as it's available, making the process more efficient and responsive.
Use ngrok during local development with Plivo to expose your local server to the internet, allowing Plivo to send webhooks to your machine. Ngrok creates a public URL that tunnels requests to your localhost, essential for testing webhook functionality before deployment.
Yes, you can send SMS messages using Plivo's Node.js SDK. The SDK simplifies interaction with the Plivo API. Include the recipient's number, the message text, and the callback URL for receiving delivery status updates.
Set up the SMS delivery report URL by specifying the url parameter in the client.messages.create() function when sending a message using the Plivo Node.js SDK. This URL points to your application's endpoint where Plivo will send the delivery status updates. The URL should use HTTPS if possible.
The Plivo Node.js SDK simplifies interaction with the Plivo API, allowing developers to easily send SMS messages, make calls, and manage other Plivo services directly from their Node.js applications. The SDK handles authentication, request formatting, and response parsing.
Verify the Plivo webhook signature using the X-Plivo-Signature-V2 header and the crypto module in Node.js. Compute the HMAC-SHA256 hash of the webhook request URL concatenated with the nonce (X-Plivo-Signature-V2-Nonce) using your Plivo Auth Token as the key. Compare this hash with the received signature using crypto.timingSafeEqual() to prevent timing attacks.
The 'Status: delivered' in a Plivo callback indicates that the SMS message was successfully delivered to the recipient's handset. This signifies a successful transmission and confirms that the message reached its intended destination, allowing your system to proceed accordingly, for example by marking the message as successfully sent in your application's database.
Plivo requires phone numbers to be in E.164 format (e.g., +14155551234) to ensure consistent and unambiguous number representation for global SMS delivery. This standardized format facilitates accurate routing and delivery of messages across different countries and carriers.
Handle multiple Message UUIDs in Plivo callbacks by processing each UUID individually, as each represents a segment of a long SMS. Update the status for each segment in your database. You might choose to track all segment statuses or focus on the first segment's status as a general indicator of delivery.
Common causes of Plivo signature verification failures include incorrect Auth Tokens, URL mismatches between the one sent to Plivo and the one reconstructed on your server, and incorrect usage of the nonce header. Double-check all parameters and ensure URL reconstruction accounts for proxies and non-standard ports.
Troubleshoot Plivo SMS sending issues by checking for accurate Plivo Auth ID, Auth Token, and Sender ID. Verify recipient numbers are in valid E.164 format and within allowed sending limits of your Plivo account type (e.g., sandbox limitations). Inspect Plivo logs for error messages and check your application logs for request failures or exceptions.
Tracking the delivery status of SMS messages is crucial for applications that rely on timely and reliable communication. Knowing whether a message reached the recipient's handset, failed, or was rejected allows you to build more robust workflows, provide better user feedback, and troubleshoot issues effectively.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via Plivo and receive real-time delivery status updates through webhooks (callbacks).
Project Goals:
Technologies Used:
dotenv: A module to load environment variables from a.envfile, keeping sensitive credentials out of source code.ngrok(for development): A tool to expose local development servers to the internet, enabling Plivo to send webhooks to your local machine.System Architecture:
The basic flow involves sending the message and receiving the status callback:
/sms/delivery-report).MessageUUID.MessageUUID, deliveryStatus, and other details.X-Plivo-Signature-V2header to ensure the request genuinely came from Plivo.200 OKstatus to acknowledge receipt of the callback.MessageUUIDfor monitoring or further processing.Prerequisites:
ngrok(Optional, for local development): Download and installngrok. (https://ngrok.com/download)1. Setting Up the Project
Let's create the project directory, initialize it, and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for your project.
mkdir plivo-sms-callbacks cd plivo-sms-callbacksInitialize Node.js Project: This creates a
package.jsonfile to manage your project's dependencies and scripts.(The
-yflag accepts the default settings)Install Dependencies: We need Express for the web server, the Plivo SDK, and
dotenvfor managing environment variables.Set Up Environment Variables: Create a file named
.envin the root of your project directory. This file will store your sensitive Plivo credentials. Never commit this file to version control.PLIVO_AUTH_ID&PLIVO_AUTH_TOKEN: Find these on your Plivo Console dashboard (https://console.plivo.com/dashboard/). Copy the Auth ID and Auth Token.PLIVO_SENDER_ID: This is the Plivo phone number (in E.164 format, e.g.,+14155551212) or Alphanumeric Sender ID you'll use to send messages. You must own this number/ID in your Plivo account.PORT: The local port your Express server will listen on.BASE_URL: The public base URL where your application will be reachable. For local development withngrok, you'll update this later.Create
.gitignore: Ensure your.envfile andnode_modulesdirectory are not committed to Git. Create a file named.gitignore:Project Structure: Your basic project structure should look like this:
We will add our application code files shortly.
2. Implementing Core Functionality: Sending SMS
We'll create a simple script to send an SMS message using the Plivo SDK. Crucially, we will specify the
urlparameter in themessages.createcall. This URL tells Plivo where to send the delivery status updates.Create
sendSms.js: Create a file namedsendSms.jsin your project root.// sendSms.js require('dotenv').config(); // Load environment variables from .env file const plivo = require('plivo'); // Retrieve credentials and sender ID from environment variables const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; const senderId = process.env.PLIVO_SENDER_ID; const baseUrl = process.env.BASE_URL; // Base URL for callbacks // Validate that environment variables are set if (!authId || !authToken || !senderId || !baseUrl) { console.error( 'Error: Plivo credentials, sender ID, or base URL not found in .env file.' ); process.exit(1); // Exit if configuration is missing } // Create Plivo client instance const client = new plivo.Client(authId, authToken); // --- Function to Send SMS --- async function sendSms(destinationNumber, messageText) { // Construct the full callback URL // This tells Plivo where to POST the delivery status const deliveryReportUrl = `${baseUrl}/sms/delivery-report`; console.log(`Sending SMS to ${destinationNumber}...`); console.log(`Configuring delivery report URL: ${deliveryReportUrl}`); try { const response = await client.messages.create( senderId, // src: Sender ID or Plivo number destinationNumber, // dst: Recipient number in E.164 format messageText, // text: The message content { url: deliveryReportUrl, // The URL for delivery status callbacks method: 'POST', // Method Plivo should use to call the URL (POST is recommended) } ); console.log('SMS Sent Successfully!'); console.log('API Response:', response); // The response contains the Message UUID(s) which link to the delivery report console.log('Message UUID(s):', response.messageUuid); } catch (error) { console.error('Error sending SMS:', error); } } // --- Example Usage --- // Get destination number and message from command line arguments // Usage: node sendSms.js <destination_number_E.164> ""Your message here"" const args = process.argv.slice(2); // Remove 'node' and script name const destination = args[0]; const text = args[1]; if (!destination || !text) { console.log( 'Usage: node sendSms.js <destination_number_E.164> ""<message_text>""' ); console.log( 'Example: node sendSms.js +12025551234 ""Hello from Plivo!""' ); process.exit(1); } // Validate E.164 format (basic check) if (!/^\+[1-9]\d{1,14}$/.test(destination)) { console.error('Error: Destination number must be in E.164 format (e.g., +12025551234)'); process.exit(1); } sendSms(destination, text);Explanation:
require('dotenv').config();: Loads variables from your.envfile intoprocess.env.new plivo.Client(): Initializes the Plivo client with your credentials.sendSms Function:deliveryReportUrlusing theBASE_URLfrom.envand a specific path (/sms/delivery-report). This is the endpoint we will create in our Express app.client.messages.create()with:src: Your Plivo sender ID/number.dst: The recipient's phone number (must be in E.164 format).text: The content of the SMS.url: The crucial parameter pointing to your webhook endpoint.method: Specifies that Plivo should use thePOSTHTTP method to send data to yoururl.POSTis generally preferred for sending data.+and digits.Testing (Initial): You can try running this now, but the callback will fail because we haven't set up the server to receive it yet.
node sendSms.js <your_recipient_phone_number> ""Test message 1""(Replace
<your_recipient_phone_number>with a valid phone number in E.164 format, preferably one you can check. If using a trial account, this must be a number verified in your Plivo Sandbox).You should see the ""SMS Sent Successfully!"" message and the Message UUID. Check the Plivo logs (https://console.plivo.com/logs/message/) – you'll likely see the message initially as ""queued"" or ""sent"", and later an attempt (and failure) by Plivo to POST to your (non-existent) callback URL.
3. Building the API Layer: Receiving Callbacks
Now, let's create the Express server and the specific endpoint (
/sms/delivery-report) to receive the delivery status callbacks from Plivo.Create
server.js: Create a file namedserver.jsin your project root.// server.js require('dotenv').config(); const express = require('express'); const crypto = require('crypto'); // Node.js crypto module for signature verification const app = express(); const port = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Middleware --- // 1. Body Parsing Middleware: // Plivo sends callbacks typically as application/x-www-form-urlencoded // Use express.urlencoded to parse this data into req.body // Important: Use { extended: true } for richer objects. // Signature V2 does not require capturing the raw body. app.use(express.urlencoded({ extended: true })); // 2. Plivo Signature Verification Middleware (CRITICAL FOR SECURITY) const verifyPlivoSignature = (req, res, next) => { const plivoSignature = req.header('X-Plivo-Signature-V2'); const nonce = req.header('X-Plivo-Signature-V2-Nonce'); const authToken = process.env.PLIVO_AUTH_TOKEN; if (!plivoSignature || !nonce || !authToken) { console.warn('Missing Plivo signature headers or auth token.'); // Don't reveal specifics about missing auth token in response return res.status(400).send('Missing signature headers'); } // Construct the full URL Plivo used to call your webhook // Note: Use req.protocol, req.hostname, and req.originalUrl // In complex proxy setups or non-standard ports, Plivo might sign the URL // including the port. req.hostname doesn't include the port. If signature // verification fails unexpectedly, try using req.get('host') instead of req.hostname, // or log the exact URL Plivo calls (visible in Plivo Debug Logs or ngrok inspector) // and ensure your reconstructed URL matches precisely. // Use req.get('host') for better compatibility behind proxies/non-standard ports. const currentHost = req.get('host'); // Includes hostname and potentially port const fullUrl = `${req.protocol}://${currentHost}${req.originalUrl}`; // Create the base string const baseString = `${fullUrl}${nonce}`; // Create the HMAC-SHA256 signature const expectedSignature = crypto .createHmac('sha256', authToken) .update(baseString) .digest('base64'); // Compare signatures using timingSafeEqual to prevent timing attacks try { // Ensure both buffers are valid before comparing const signatureBuffer = Buffer.from(plivoSignature); const expectedBuffer = Buffer.from(expectedSignature); if (signatureBuffer.length !== expectedBuffer.length) { console.error('Invalid Plivo signature length.'); return res.status(403).send('Invalid signature'); } if (crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) { console.log('Plivo signature verified successfully.'); next(); // Signature matches, proceed to the route handler } else { console.error('Invalid Plivo signature.'); res.status(403).send('Invalid signature'); // Forbidden } } catch (error) { console.error('Error during signature comparison:', error); res.status(400).send('Invalid signature format'); // Bad request if buffers are bad } }; // --- Routes --- // 1. Health Check Route app.get('/health', (req, res) => { res.status(200).send('OK'); }); // 2. SMS Delivery Report Route // Apply the signature verification middleware ONLY to this route app.post('/sms/delivery-report', verifyPlivoSignature, (req, res) => { // The request body now contains the parsed form data const deliveryData = req.body; console.log('--- Received Plivo Delivery Report ---'); console.log('Timestamp:', new Date().toISOString()); console.log('Status:', deliveryData.Status); console.log('Message UUID:', deliveryData.MessageUUID); console.log('From:', deliveryData.From); // Sender ID used console.log('To:', deliveryData.To); // Recipient number // Add more fields as needed (e.g., ErrorCode, MessageTime) // See Plivo docs for all possible parameters: // https://www.plivo.com/docs/messaging/concepts/message-delivery-reports/#delivery-report-parameters console.log('Full Payload:', JSON.stringify(deliveryData, null, 2)); // Log full payload nicely console.log('------------------------------------'); // --- Add your business logic here --- // - Update your database with the status for the MessageUUID // - Trigger notifications or further actions based on the status // - Example: If Status is 'failed' or 'undelivered', enqueue a retry or notify support. // Acknowledge receipt to Plivo // You MUST send a 2xx status code (e.g., 200 OK) quickly, // otherwise Plivo may consider the callback failed and retry. res.status(200).send('Delivery report received.'); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening on port ${port}`); console.log(`Webhook endpoint ready at /sms/delivery-report (POST)`); console.log(`Health check endpoint ready at /health (GET)`); });Explanation:
express.urlencoded({ extended: true }): Parses incoming requests withContent-Type: application/x-www-form-urlencoded. Theextended: trueoption allows for rich objects and arrays to be encoded into the URL-encoded format. Plivo V2 signature verification does not require the raw request body.verifyPlivoSignature: This custom middleware is essential for security.X-Plivo-Signature-V2andX-Plivo-Signature-V2-Nonceheaders sent by Plivo, along with thePLIVO_AUTH_TOKENfrom environment variables.req.protocol,req.get('host')(which includes hostname and port, making it more robust behind proxies), andreq.originalUrl.baseStringby concatenating the full URL and the nonce.crypto.createHmac('sha256', ...)with yourPLIVO_AUTH_TOKENand thebaseString.crypto.timingSafeEqual(important to prevent timing attacks). Atry...catchblock handles potential errors during comparison (e.g., invalid base64), and an explicit length check is added beforetimingSafeEqual.next()to proceed to the route handler. Otherwise, it sends a403 Forbiddenor400 Bad Requestresponse./health(GET): A simple endpoint to check if the server is running./sms/delivery-report(POST):verifyPlivoSignaturemiddleware before the main handler.req.body), which includesStatus,MessageUUID,From,To, and potentiallyErrorCode.200 OKstatus code promptly to acknowledge receipt. Failure to do so might cause Plivo to retry the callback.4. Integrating with Plivo & Local Testing (
ngrok)To receive callbacks on your local machine during development, you need a way to expose your local server to the public internet.
ngrokis perfect for this.Start Your Local Server: Open a terminal in your project directory and run:
You should see
Server listening on port 3000....Start
ngrok: Open a second terminal window (leave the server running) and startngrok, telling it to forward to the port your server is running on (e.g., 3000).Get Your Public
ngrokURL:ngrokwill display output similar to this:Copy the
https://forwarding URL (e.g.,https://<random_string>.ngrok.io). This is your temporary public URL. Using HTTPS is strongly recommended.Update
.env: Go back to your.envfile and update theBASE_URLwith your publicngrokHTTPS URL.Restart Your Server (Important): Stop your Node.js server (
Ctrl+Cin the first terminal) and restart it (node server.js) after changing the.envfile to ensure it picks up the newBASE_URL.Send Another Test SMS: Now, run the
sendSms.jsscript again with a valid recipient number.node sendSms.js <your_recipient_phone_number> ""Test message with callback""Verify Callback:
node server.jsis running. Within a few seconds to minutes (depending on the carrier), you should see the log output from the/sms/delivery-reportroute, including ""Plivo signature verified successfully."", theStatus(e.g.,delivered,sent,failed,undelivered), andMessageUUID.ngrokLogs: The terminal runningngrokwill show incomingPOST /sms/delivery-reportrequests with a200 OKresponse. You can also inspect requests in detail via thengrokweb interface (usuallyhttp://127.0.0.1:4040).200 OKstatus.5. Error Handling, Logging, and Retry Mechanisms
sendSms.jsscript includes basictry...catcharound the Plivo API call. Plivo errors often include specific codes. Refer to Plivo API error documentation: https://www.plivo.com/docs/api/overview/#api-status-codesserver.jshandles signature verification errors (400/403). Other potential errors (like database issues if you add them) should be caught within the route handler, logged, but still ideally return a2xxcode to Plivo unless the request itself was malformed (4xx). Avoid5xxresponses if possible, as Plivo might retry aggressively.console.logfor simplicity. For production, use a more robust logging library likewinstonorpino.sendSms.jsfails due to a temporary network issue or a Plivo server error (5xx), you might implement a simple retry logic (e.g., usingasync-retry) with exponential backoff.2xxwithin a timeout period (typically ~5 seconds). Your responsibility is to ensure your endpoint is reliable, responds quickly (under the timeout), and handles requests idempotently (if necessary, though usually not required for simple status logging based on uniqueMessageUUID).6. Database Schema and Data Layer (Conceptual)
While this guide doesn't implement a database, here's how you would typically integrate it:
Schema: You'd likely have a table to store message details and their status updates.
-- Example PostgreSQL Schema CREATE TABLE sms_messages ( message_uuid UUID PRIMARY KEY, -- Plivo's MessageUUID sender_id VARCHAR(50) NOT NULL, recipient_number VARCHAR(20) NOT NULL, message_text TEXT, initial_api_response JSONB, -- Store the response from messages.create status VARCHAR(50) DEFAULT 'queued', -- e.g., queued, sent, delivered, failed, undelivered status_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, error_code VARCHAR(10), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Index for efficient status lookups CREATE INDEX idx_sms_messages_status ON sms_messages(status); CREATE INDEX idx_sms_messages_recipient ON sms_messages(recipient_number);Integration:
sendSms.js: After successfully callingclient.messages.create, insert a new record intosms_messageswith themessageUuid, recipient, sender, text, initial response, and setstatusto 'queued' or 'sent' based on the API response.server.js(/sms/delivery-report):MessageUUIDandStatus(andErrorCodeif present) fromreq.body.sms_messagesusing theMessageUUID.status,status_timestamp, anderror_codefor that record.Data Layer: Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js) to interact with your database safely and efficiently.
7. Security Features
Webhook Signature Verification: Implemented and crucial. This prevents attackers from sending fake callbacks to your endpoint. Always use the latest signature version offered by Plivo (V2 currently).
Environment Variables: Keep
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, and any other secrets out of your code and.envfiles out of version control.HTTPS: Always use HTTPS for your callback URL (
ngrokprovides this, and production deployments must use TLS/SSL). This encrypts the data in transit.Input Validation (Sending): If the destination number or message text in
sendSms.jscomes from user input, validate and sanitize it thoroughly to prevent injection attacks or abuse. Ensure numbers are in E.164 format.Rate Limiting: Apply rate limiting to your callback endpoint (
/sms/delivery-report) using middleware likeexpress-rate-limitto prevent abuse or accidental denial-of-service if Plivo retries excessively for some reason.// Example in server.js (install: npm install express-rate-limit) const rateLimit = require('express-rate-limit'); const callbackLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 500, // Limit each IP to 500 requests per windowMs (adjust as needed) message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply to the callback route *before* signature verification app.post('/sms/delivery-report', callbackLimiter, verifyPlivoSignature, (req, res) => { // ... handler logic });Firewall: Configure server firewalls to only allow traffic on necessary ports (e.g., 443 for HTTPS). You might restrict access to the callback endpoint to only known Plivo IP addresses if feasible and documented by Plivo, but signature verification is generally the preferred and more flexible approach.
8. Handling Special Cases
dst) to be in E.164 format (e.g.,+14155551234). Ensure any user input is normalized to this format before sending.messages.createresponse might contain multiplemessageUuids. The delivery report callback will typically be sent per segment, each with its correspondingMessageUUID. Your database schema and logic should handle this many-to-one relationship if segment-level tracking is needed. Often, tracking the status of the first UUID is sufficient to know the overall message delivery attempt status.Statusvalues (queued,sent,delivered,failed,undelivered) and any associatedErrorCodevalues. See Plivo documentation for details. Build your application logic accordingly./sms/delivery-reportendpoint responds quickly (within Plivo's timeout, typically ~5 seconds, ideally under 1-2 seconds) with a200 OK. Long-running tasks should be offloaded to a background job queue (e.g., BullMQ, Kue) triggered by the callback, rather than being executed synchronously within the request handler.9. Performance Optimizations
/sms/delivery-reporthandler fast. Do minimal work: verify signature, parse data, acknowledge receipt (200 OK), and potentially enqueue a background job for heavier processing (like database updates, notifications).async/awaitfor I/O operations (like database calls, though ideally offloaded).message_uuidis indexed (ideally primary key) for fast lookups when updating status. Index other commonly queried fields likestatusorrecipient_number.k6,artillery, orautocannonto test how many concurrent callbacks your server can handle.clustermodule or a process manager likepm2in cluster mode to utilize multiple CPU cores.10. Monitoring, Observability, and Analytics
/healthendpoint provides a basic check. Production monitoring systems (like Prometheus/Grafana, Datadog, New Relic) should poll this endpoint.delivered,failed, etc.)./sms/delivery-reporthandler).failed/undeliveredstatuses.11. Troubleshooting and Caveats
401 Unauthorizederrors when sending SMS or signature verification failures. Double-check credentials in.envand the Plivo console.src(Sender ID): Ensure thePLIVO_SENDER_IDis a valid Plivo number (in E.164) or an approved Alphanumeric Sender ID associated with your account. Using an unowned number results in errors.dst(Recipient Number): Must be E.164 format. Sending to incorrectly formatted numbers will fail. Trial accounts can only send to numbers verified in the Sandbox.localhost. Usengrokfor local testing or deploy to a public server.sendSms: Ensure theurlparameter matches the endpoint route inserver.jsand uses the correctBASE_URL. Check for typos.ngrokand production URLs. Plivo may not send callbacks to HTTP URLs.node server.jsis running when expecting callbacks.200 OK: If your endpoint errors out (5xx) or times out, Plivo won't know you received the data and will retry, potentially causing duplicate processing if your handler isn't idempotent. Log errors but try to return200 OK.PLIVO_AUTH_TOKENis correct in your.envfile and matches the token in the Plivo Console.${req.protocol}://${req.get('host')}${req.originalUrl}) exactly matches what Plivo used. Checkngrokinspector or server logs for the exact URL Plivo called. Proxies or non-standard ports can sometimes alter hostname/protocol headers. Usingreq.get('host')is generally more robust thanreq.hostname.X-Plivo-Signature-V2,X-Plivo-Signature-V2-Nonce) are being read and used.express.urlencoded({ extended: true })middleware is used before your route handler if theContent-Typeisapplication/x-www-form-urlencoded(which is typical for Plivo callbacks). If Plivo were sending JSON (application/json), you would useexpress.json()instead.