Receive SMS with Plivo in Node.js and Express: Two-Way Messaging Guide
Learn how to receive SMS messages in Node.js using Plivo and Express. Complete tutorial covering webhook setup, signature validation, auto-replies, and deployment for production two-way messaging.
Learn how to receive SMS messages in Node.js using Plivo's SMS API and Express framework. This comprehensive guide walks you through building a production-ready application that handles inbound SMS webhooks, validates signatures, and sends automated replies—perfect for customer support bots, appointment confirmations, and interactive messaging systems.
When someone sends an SMS to your Plivo phone number, Plivo forwards it to your Express application via HTTP webhook. You'll learn how to set up your project, configure the webhook endpoint, process incoming messages, generate XML responses, secure your application, and deploy it to production. By the end, you'll have a functional Express application that receives SMS messages and responds automatically.
Common Use Cases
Use Case
Complexity
Implementation Time
Auto-reply with keywords (HELP, STOP)
Simple
1–2 hours
Appointment confirmation system
Moderate
4–6 hours
Customer support bot with conversation state
Complex
2–3 days
Order tracking notifications
Moderate
3–5 hours
Technologies Used
Node.js: A JavaScript runtime environment for building server-side applications. Recommended: Node.js v22 LTS (Long Term Support began October 2024, maintained until April 2027).
Express.js: A minimal and flexible Node.js web application framework (v4.16+ includes necessary body parsing middleware built-in via express.urlencoded() and express.json()).
Plivo: A cloud communications platform providing SMS API services.
Plivo Node.js SDK: Simplifies interaction with the Plivo API and XML generation. Latest version: 4.74.0 (as of January 2025).
ngrok (for development): A tool to expose local development servers to the internet. Note: The free tier includes 1 static domain (available since August 2023), eliminating the need to update URLs constantly during development sessions.
dotenv: A module to load environment variables from a .env file.
System Architecture
graph LR
A[End User] -- Sends SMS --> B(Plivo Phone Number);
B -- Forwards SMS via HTTP POST --> C{Your Express App Webhook};
C -- Processes Request --> C;
C -- Generates Plivo XML Response --> C;
C -- Sends XML Response --> B;
B -- Sends Reply SMS --> A;
style C fill:#f9f,stroke:#333,stroke-width:2px
How It Works
User sends SMS – An end user texts your Plivo phone number
Plivo forwards webhook – Plivo sends an HTTP POST request to your Express app's /receive_sms endpoint
App processes message – Your application validates the signature, processes the message, and generates a response
XML response – Your app returns Plivo XML containing the reply message
Plivo sends reply – Plivo delivers your reply SMS to the user
Prerequisites
Before you can receive SMS in Node.js with Plivo, you'll need:
An SMS-enabled Plivo phone number (Purchase via the Plivo console: Phone Numbers > Buy Numbers).
Node.js (v22 LTS recommended, minimum v18+) and npm (or yarn) installed on your system.
Basic understanding of JavaScript and Node.js.
ngrok installed for local development testing (Download ngrok).
1. Setting Up Your Node.js Project to Receive SMS
Create a new Node.js project and install the dependencies needed to receive SMS messages with Plivo and Express.
Create Project Directory:
Create a new directory for your project, then navigate into it.
Bash
mkdir plivo-node-sms-inbound
cd plivo-node-sms-inbound
Initialize Node.js Project:
Initialize the project using npm to create a package.json file.
Bash
npm init -y
The -y flag accepts the default settings.
Install Dependencies:
Install Express for the web server, the Plivo Node.js SDK, and dotenv for managing environment variables. Modern Express (v4.16+) includes the necessary middleware for parsing URL-encoded bodies, so body-parser is no longer required separately.
Bash
npm install express plivo dotenv
express: The web framework. Includes express.urlencoded() middleware.
plivo: The official Plivo Node.js SDK.
dotenv: Loads environment variables from a .env file into process.env.
Create Project Structure:
Create the basic files and directories.
Bash
touch server.js .env .gitignore
server.js: Contains your main application code.
.env: Stores sensitive credentials like your Plivo Auth Token. Never commit this file to version control.
.gitignore: Specifies intentionally untracked files that Git should ignore.
Configure .gitignore:
Add node_modules and .env to your .gitignore file to prevent committing them.
Text
# .gitignore
node_modules/
.env
Set Up Environment Variables (.env):
Add your Plivo Auth ID and Auth Token for webhook validation. Find these on your Plivo Console dashboard.
.env
# .env
# Plivo Credentials (Find on Plivo Console Dashboard)
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
# Server Configuration
PORT=3000
Replace YOUR_PLIVO_AUTH_ID and YOUR_PLIVO_AUTH_TOKEN with your actual credentials.
PORT: The port your local server will run on.
2. Creating Your Express SMS Webhook Handler
Build the Express application to receive SMS webhooks from Plivo. When someone texts your Plivo number, Plivo sends an HTTP POST request to your webhook endpoint. This section shows you how to handle incoming messages and send automated replies.
Understanding How Plivo SMS Webhooks Work
A webhook is an HTTP callback that Plivo uses to notify your application about incoming messages. When Plivo receives an SMS at your number, it immediately sends an HTTP POST request to your configured Message URL. Your Express app processes the request and responds with XML instructions telling Plivo what to do next—such as sending a reply SMS back to the sender.
server.js:
JavaScript
// server.js// 1. Load Environment Variablesrequire('dotenv').config();
// 2. Import Dependenciesconst express = require('express');
const plivo = require('plivo');
const crypto = require('crypto'); // Required internally by Plivo SDK for webhook validation// 3. Initialize Express Appconst app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000// 4. Middleware Setup// Use built-in Express middleware to parse URL-encoded bodies (which Plivo uses)
app.use(express.urlencoded({ extended: true }));
// Middleware for Plivo Webhook Validation (Crucial Security Step)constvalidatePlivoSignature = (req, res, next) => {
const signature = req.header('X-Plivo-Signature-V3');
const nonce = req.header('X-Plivo-Signature-V3-Nonce');
// Construct the URL Plivo used to call the webhook.// Note: This reconstruction might be fragile behind certain reverse proxies or load balancers.// Verify this matches how Plivo sees the URL, or consult Plivo docs for alternative validation methods if issues arise.const url = req.protocol + '://' + req.get('host') + req.originalUrl;
const method = req.method; // Should be POST for Plivo webhooksif (!signature || !nonce) {
console.warn('Missing Plivo signature headers');
return res.status(400).send('Missing signature headers');
}
try {
// Use the Plivo SDK's validateV3Signature method (as of SDK v4.74.0, January 2025)// This validates using HMAC-SHA256 with your Auth Token, protecting against replay attacksconst valid = plivo.validateV3Signature(method, url, nonce, process.env.PLIVO_AUTH_TOKEN, signature);
if (valid) {
console.log('Plivo signature validation successful.');
next(); // Signature is valid, proceed to the route handler
} else {
console.warn('Invalid Plivo signature.');
return res.status(403).send('Invalid signature'); // Forbidden
}
} catch (error) {
console.error('Error during Plivo signature validation:', error);
return res.status(500).send('Signature validation error');
}
};
// 5. Define Webhook Route Handler// Apply the validation middleware ONLY to the Plivo webhook route
app.post('/receive_sms', validatePlivoSignature, (req, res) => {
// Plivo sends message details in the request bodyconst from_number = req.body.From;
const to_number = req.body.To; // Your Plivo numberconst text = req.body.Text;
const message_uuid = req.body.MessageUUID; // Unique ID for the incoming messageconsole.log(`Message received on ${to_number} from ${from_number}: "${text}" (UUID: ${message_uuid})`);
// --- Business Logic Here ---// Example: Simple auto-reply based on keyword// Using template literals for cleaner string constructionlet reply_text = `Thank you for your message! We received: "${text}"`;
if (text && text.toLowerCase().includes('help')) {
reply_text = `Need help? Contact support@example.com or call 1-800-555-HELP.`;
} elseif (text && text.toLowerCase().includes('stop')) {
reply_text = `You have been unsubscribed. No more messages will be sent.`;
// Add logic here to actually unsubscribe the user in your system
}
// --- End Business Logic ---// 6. Generate Plivo XML Response// Use the Plivo SDK to create a <Response> XML objectconst response = new plivo.Response();
// Add a <Message> element to the response to send an SMS backconst params = {
src: to_number, // The reply comes FROM your Plivo numberdst: from_number // The reply goes TO the sender's number
};
response.addMessage(reply_text, params);
// 7. Send XML Response// Convert the response object to XML and set the correct Content-Typeconst xmlResponse = response.toXML();
console.log(`Sending XML Response:\n`, xmlResponse);
res.setHeader('Content-Type', 'application/xml');
res.status(200).send(xmlResponse);
});
// Basic health check endpoint (optional but good practice)
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// 8. Start the Server (Only if this file is run directly)// This check allows the 'app' object to be exported for testing without starting the serverif (require.main === module) {
app.listen(port, () => {
console.log(`Server running on port ${port}`);
console.log(`Plivo Auth ID: ${process.env.PLIVO_AUTH_ID ? 'Loaded' : 'MISSING!'}`);
console.log(`Plivo Auth Token: ${process.env.PLIVO_AUTH_TOKEN ? 'Loaded' : 'MISSING!'}`);
console.log('Make sure ngrok is running and pointed to this port for development testing.');
console.log(`Webhook URL should be configured in Plivo as: http://<your-ngrok-url>/receive_sms`);
});
}
// Export the app for testing purposesmodule.exports = app;
Code Explanation
Load Environment Variables:require('dotenv').config() loads variables from .env.
Import Dependencies: Imports Express, the Plivo SDK, and Node's crypto module (needed internally by the SDK's validation function).
Initialize App: Creates the Express application instance.
Middleware:
express.urlencoded: Parses the incoming webhook data from Plivo using Express's built-in middleware.
validatePlivoSignature: Crucial security middleware. It verifies that incoming requests genuinely originated from Plivo using the X-Plivo-Signature-V3 header, a nonce, your Auth Token, and the request details. This prevents attackers from spoofing requests to your webhook. It's applied only to the /receive_sms route. A note about potential URL reconstruction issues behind proxies is included.
Webhook Route (/receive_sms):
This endpoint (using HTTP POST) is what Plivo calls when an SMS arrives.
It first runs the validatePlivoSignature middleware.
It extracts relevant information (From, To, Text, MessageUUID) from req.body.
Includes a placeholder for your business logic using template literals for replies.
Generate Plivo XML:
Creates a plivo.Response object.
Uses response.addMessage(reply_text, params) to add a <Message> tag to the XML. This tag instructs Plivo to send an SMS reply.
src (source) is your Plivo number (to_number from the incoming message).
dst (destination) is the original sender's number (from_number).
Send XML Response:
Converts the Plivo response object to an XML string using response.toXML().
Sets the Content-Type header to application/xml.
Sends the XML back to Plivo with a 200 OK status. Plivo processes this XML to send the reply SMS.
Start Server: Listens on the configured port, but only when server.js is executed directly (not when required by tests).
Export App: The app object is exported so it can be imported by testing frameworks like supertest.
3. Building a Complete API Layer
In this specific scenario, the "API" is the single webhook endpoint (/receive_sms) that Plivo interacts with. We've already implemented:
Authentication/Authorization: Handled via Plivo's webhook signature validation (validatePlivoSignature middleware). Only requests signed correctly with your Plivo Auth Token are processed.
Request Validation: Basic validation happens via signature checking. Add more specific validation (e.g., checking expected fields, data types) within the /receive_sms route handler if needed.
Headers (Required from Plivo):X-Plivo-Signature-V3, X-Plivo-Signature-V3-Nonce
Request Body Parameters (from Plivo):
From: Sender's phone number (E.164 format).
To: Your Plivo number receiving the message (E.164 format).
Text: The content of the SMS message.
Type: sms
MessageUUID: Unique identifier for the incoming message.
(Other potential parameters like Encoding, ParentMessageUUID, etc. – see Plivo docs)
Response Content-Type:application/xml
Response Body (Example Success):
XML
<?xml version="1.0" encoding="utf-8"?><Response><Messagesrc="+14155551212"dst="+14155559876">Thank you for your message! We received: "Hello"</Message></Response>
Response Body (Error – Invalid Signature): Status 403 Forbidden, Body: Invalid signature
Response Body (Error – Missing Headers): Status 400 Bad Request, Body: Missing signature headers
Testing with cURL (Without Valid Signature)
You can simulate a POST request, but without a valid signature, it should be rejected by your validation middleware.
Bash
curl -X POST http://localhost:3000/receive_sms \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "From=+15551234567&To=+15557654321&Text=Hello+from+curl&MessageUUID=abc-123"# Expected Response (if validation is working):# Invalid signature (or Missing signature headers) with a 403 or 400 status code
Proper testing requires sending a real SMS via Plivo or using Plivo's API console/tools that generate correct signatures.
4. Configuring Plivo to Send Webhooks to Your Express App
Connect your Express application to Plivo by configuring a Message URL. During development, use ngrok to expose your local server. Note: The ngrok free tier includes 1 static domain (available since August 2023), keeping your webhook URL consistent. For production, deploy to a server with a stable HTTPS URL.
What Is a Plivo Application?
A Plivo Application tells Plivo where to forward incoming SMS messages. When someone texts your Plivo number, the Application's Message URL determines which webhook endpoint receives the data. Think of it as the routing configuration between your Plivo phone number and your Express server.
Run Your Local Server:
Ensure your .env file is populated with your Plivo credentials.
Bash
node server.js
You should see output indicating the server is running on port 3000 (or your configured port).
Expose Local Server with ngrok (Development Only):
Open a new terminal window and run ngrok to create a public URL tunnel to your local server's port.
Bash
ngrok http 3000
ngrok displays 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://xxxxxxxxxxxx.ngrok.io -> http://localhost:3000
Forwarding https://xxxxxxxxxxxx.ngrok.io -> http://localhost:3000
Copy the https:// forwarding URL (e.g., https://xxxxxxxxxxxx.ngrok.io). This is your temporary public webhook URL for Plivo during development.
Create a Plivo Application:
Log in to the Plivo Console.
Navigate to Messaging → Applications → XML.
Click the "Add New Application" button.
Application Name: Give it a descriptive name (e.g., Node Express SMS Handler).
Message URL: Paste your https:// ngrok forwarding URL and append your webhook path: https://xxxxxxxxxxxx.ngrok.io/receive_sms (Replace with your actual ngrok URL or production URL later).
Method: Select POST.
Hangup URL / Fallback Answer URL: Leave blank for this SMS-only application.
Click "Create Application".
Assign the Application to Your Plivo Number:
Navigate to Phone Numbers → Your Numbers.
Click on the SMS-enabled Plivo number you want to use for receiving messages.
In the number configuration screen, find the "Application Type" section. Select XML Application.
From the "Plivo Application" dropdown, select the application you just created (Node Express SMS Handler).
Click "Update Number".
Your Express application is now linked to your Plivo number. When an SMS is sent to that number, Plivo makes a POST request to your configured URL (ngrok for dev, production URL for live), which forwards it to your server.js application.
Common Configuration Issues
Issue
Solution
Webhook URL typo
Double-check the URL includes https:// and /receive_sms
Method not POST
Verify the Plivo Application uses POST method
Number not assigned
Confirm Application is linked to your Plivo number
ngrok not running
Ensure ngrok is active and pointing to port 3000
5. Error Handling and Logging
Your current application has basic logging and webhook validation error handling. Enhance the route handler with internal error catching.
Consistent Error Strategy: Return specific HTTP statuses (400, 403, 500) for signature errors. For internal processing errors within the /receive_sms route after validation, wrap your logic in a try...catch block.
Logging:console.log provides basic information. For production, use more robust logging libraries like Winston or Pino, which enable structured logging, different log levels (info, warn, error), and routing logs to files or external services.
Enhanced Error Handling
JavaScript
// Inside server.js – Refined Route Handler
app.post('/receive_sms', validatePlivoSignature, (req, res) => {
try { // Start try block after validationconst from_number = req.body.From;
const to_number = req.body.To; // Your Plivo numberconst text = req.body.Text;
const message_uuid = req.body.MessageUUID; // Unique ID for the incoming message// Basic check for missing essential data (optional, but good practice)if (!from_number || !to_number || !message_uuid) {
console.warn(`Incomplete message data received: From=${from_number}, To=${to_number}, UUID=${message_uuid}`);
// Respond 200 OK to Plivo to prevent retries, but don't process further.return res.status(200).send(new plivo.Response().toXML());
}
console.log(`Message received on ${to_number} from ${from_number}: "${text || '(empty)'}" (UUID: ${message_uuid})`);
// --- Business Logic Here ---let reply_text = `Thank you for your message! We received: "${text || '(empty)'}"`;
if (text && text.toLowerCase().includes('help')) {
reply_text = `Need help? Contact support@example.com or call 1-800-555-HELP.`;
} elseif (text && text.toLowerCase().includes('stop')) {
reply_text = `You have been unsubscribed. No more messages will be sent.`;
// Add unsubscribe logicconsole.log(`STOP keyword received from ${from_number}. Unsubscribe logic should run.`);
}
// --- End Business Logic ---const response = new plivo.Response();
const params = { src: to_number, dst: from_number };
response.addMessage(reply_text, params);
const xmlResponse = response.toXML();
console.log(`Sending XML Response:\n`, xmlResponse);
res.setHeader('Content-Type', 'application/xml');
res.status(200).send(xmlResponse);
} catch (error) { // Catch unexpected internal processing errorsconsole.error(`Error processing incoming SMS ${req.body?.MessageUUID || 'UNKNOWN'}:`, error);
// Avoid sending sensitive error details back.// Sending an empty <Response/> tells Plivo we handled it but won't reply.const errorResponse = new plivo.Response();
res.setHeader('Content-Type', 'application/xml');
// Respond 200 to Plivo to acknowledge receipt and prevent Plivo retries,// even though an internal server error occurred. Log the error for debugging.
res.status(200).send(errorResponse.toXML());
// Alternatively, if you want Plivo to retry due to a transient error:// res.status(500).send('Internal Server Error'); // Use with caution regarding retries.
}
});
Plivo Retry Behavior
Plivo automatically retries webhooks three times if it does not receive a 200 OK status code (as of January 2025):
Retry Attempt
Timing
First retry
60 seconds after original attempt
Second retry
120 seconds after first retry
Third retry
240 seconds after second retry
By responding 200 OK even in our catch block (with an empty XML response), we tell Plivo we've received the webhook and prevent retries for that specific failure. If the failure is transient and you want Plivo to retry, respond with a 5xx status code. Be cautious, as this could lead to duplicate processing if not handled carefully.
Note: Plivo offers configurable callback retry options (announced May 2024) that let you customize timeout values and retry policies by appending parameters to webhook URLs (e.g., #ct=2000&rt=3000 for connection and read timeouts).
6. Database Schema and Data Layer (Conceptual)
This simple auto-responder doesn't require a database. However, more complex interactions need one:
Use Cases: Store conversation history, user profiles, subscription status, order details, etc.
Schema Example (Conceptual – e.g., for tracking conversations):
Data Access: Use an ORM like Prisma or Sequelize, or a database driver (e.g., pg for PostgreSQL, mysql2 for MySQL) to interact with the database within your /receive_sms logic. Typically fetch relevant conversation context based on the from_number and to_number before deciding on a reply.
Migrations: Use the migration tools provided by your ORM (e.g., prisma migrate dev, sequelize db:migrate) to manage schema changes.
7. Security Features
Security is paramount when exposing webhooks to the internet.
Webhook Signature Validation: Already implemented (validatePlivoSignature). This is the most critical security measure. It ensures requests are authentic and originated from Plivo.
Input Sanitization: While Plivo handles SMS encoding, if you use the Text input in database queries or complex dynamic responses, sanitize it to prevent injection attacks (SQL injection, XSS). Libraries like validator.js can help. For basic replies like this example, the risk is lower.
Rate Limiting: Protect your endpoint from abuse or denial-of-service attacks by limiting the number of requests from a single IP or for a specific Plivo number. Use middleware like express-rate-limit.
Bash
npm install express-rate-limit
JavaScript
// server.js (add near other middleware, before routes)const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: 'Too many requests, please try again later.',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply the rate limiting middleware to the SMS webhook route
app.use('/receive_sms', limiter);
HTTPS:Always use HTTPS for your webhook URL in production.ngrok provides this automatically for development. In production, ensure your server is configured behind a reverse proxy (like Nginx or Apache) or load balancer that handles SSL/TLS termination, or use a PaaS that provides it. Plivo requires HTTPS for production webhooks.
Environment Variables: Keep sensitive information (Plivo Auth Token, database credentials) out of your code and use environment variables (.env locally, system environment variables in production). Ensure .env is in .gitignore.
Common Vulnerabilities: Be aware of OWASP Top 10 vulnerabilities and how they might apply, especially if you add database interactions or more complex logic.
Security Checklist
Webhook signature validation enabled
Rate limiting configured
HTTPS enforced in production
Environment variables secured
Input sanitization for database queries
.env file in .gitignore
Error messages don't expose sensitive data
8. Handling Special Cases
Message Encoding: Plivo handles standard SMS encodings (GSM-7, UCS-2). The Text field in the webhook payload contains the decoded message content. Be aware of character limits (160 for GSM-7, 70 for UCS-2 per segment).
Concatenated Messages (Long SMS): Longer messages are split into multiple segments by carriers. Plivo usually forwards these as separate webhook requests. To reassemble them, look for parameters like ParentMessageUUID (identifies the original message) and potentially sequence number parameters in the webhook payload. Buffer these parts (e.g., in memory, cache, or database) until all segments arrive before processing the full message. Consult the Plivo documentation for the exact parameters and recommended handling logic for message concatenation.
Time Zones: Timestamps from Plivo are in UTC. Store timestamps in your database in UTC and convert to the user's local time zone for display if necessary.
"STOP" / Opt-Out: Plivo handles standard opt-out keywords (STOP, UNSUBSCRIBE, etc.) at the carrier level for Toll-Free and Short Code numbers if enabled in your Plivo number's configuration. However, you must also implement logic in your application to honor these requests (e.g., flagging the user as unsubscribed in your database) to comply with regulations (like TCPA in the US). Our example includes a basic keyword check but requires adding the actual database/system update logic.
Rate Limits / Throttling: Plivo enforces rate limits on sending messages. Your application might also be throttled by Plivo if it responds too slowly or incorrectly too often. Ensure your webhook responds quickly (ideally under 1–2 seconds; Plivo timeout is 5 seconds).
Duplicate Messages: Network issues can occasionally cause Plivo to send duplicate webhooks (especially if your server doesn't respond promptly). Use the MessageUUID to deduplicate messages by checking if you've already processed that UUID in your database or a cache (like Redis) within a short time window before processing.
Character Encoding Limits
Encoding
Characters per Segment
Use Case
GSM-7
160
Standard English text
UCS-2
70
Emoji, special characters, non-Latin scripts
9. Performance Optimizations
For this simple application, performance is less critical, but consider these for scaling:
Fast Webhook Response: The most crucial optimization. Do minimal work synchronously within the webhook handler. If extensive processing is needed (calling external APIs, complex database queries), acknowledge the webhook quickly (send the XML response) and perform the heavy lifting asynchronously using a job queue (e.g., BullMQ, Kue with Redis) or background process.
Caching: Cache frequently accessed data (e.g., user profiles, common replies) using Redis or Memcached to reduce database load.
Efficient Database Queries: Index database columns used in lookups (e.g., user_phone_number, plivo_message_uuid).
Load Testing: Use tools like k6, Artillery, or ApacheBench (ab) to simulate high traffic to your webhook endpoint and identify bottlenecks.
Node.js Clustering: Use Node.js's built-in cluster module or a process manager like PM2 in cluster mode to run multiple instances of your application across CPU cores, improving throughput.
Performance Targets
Metric
Target
Critical Threshold
Webhook response time
< 1 second
< 2 seconds
Success rate
> 99.5%
> 99%
Concurrent requests
100+ req/sec
50+ req/sec
10. Monitoring, Observability, and Analytics
Health Checks: The /health endpoint provides a basic check. Monitoring services (like UptimeRobot, Pingdom, AWS CloudWatch) can ping this endpoint to ensure your application is running.
Performance Metrics (APM): Use Application Performance Monitoring tools (e.g., Datadog, New Relic, Dynatrace, Sentry APM) to track response times, error rates, resource usage (CPU, memory), and Node.js-specific metrics (event loop lag). These tools often auto-instrument Express applications.
Error Tracking: Integrate services like Sentry or Bugsnag to capture, aggregate, and alert on application errors (going beyond basic console logging).
Logging Aggregation: Ship logs (using Winston/Pino transports) to a centralized logging platform (e.g., ELK stack, Datadog Logs, Logz.io, Sematext) for easier searching, analysis, and alerting based on log patterns.
Key Metrics Dashboard: Create dashboards (in your APM, logging tool, or Grafana) showing:
Incoming message rate
Webhook response time (average, p95, p99)
Webhook error rate (4xx, 5xx)
Signature validation failures
Message processing latency (if using async processing)
Alerting: Configure alerts based on thresholds (e.g., error rate > 1%, response time > 2 seconds, health check fails).
Essential Metrics to Track
Category
Metrics
Performance
Response time (avg, p95, p99), throughput (messages/min)
Reliability
Success rate, error rate, uptime percentage
Security
Signature validation failures, rate limit hits
Business
Messages received, unique users, keyword usage
11. Troubleshooting and Caveats
ngrok Issues (Development)
Ensure ngrok is running and pointing to the correct local port (3000 in this example).
Free tier users: As of August 2023, the free tier includes 1 static domain that persists across sessions. If using a random domain (non-static), the URL expires and requires updating in Plivo.
Firewalls might block ngrok traffic.
Plivo Configuration Errors
Incorrect Message URL: Double-check for typos, correct protocol (HTTPS required for production), and ensure the /receive_sms path is included.
Incorrect Method: Ensure the method is set to POST in the Plivo Application.
Number Not Assigned: Verify the Plivo Application is correctly assigned to the intended Plivo phone number.
Number Not SMS Enabled: Ensure the Plivo number is capable of sending/receiving SMS in the relevant region.
Webhook Signature Validation Failures
Incorrect Auth Token: Verify the PLIVO_AUTH_TOKEN in your .env file or production environment variables exactly matches the one in the Plivo console.
URL Mismatch: Ensure the URL constructed in the validation logic matches precisely how Plivo calls your webhook (check ngrok logs, Plivo debug logs, or your server access logs). Proxies can sometimes alter URLs/host headers.
Implementation Error: Double-check the usage of plivo.validateV3Signature.
Code Errors
Check server logs (node server.js output or aggregated logs) for runtime errors.
Ensure all dependencies are installed (npm install).
Verify XML generation logic (response.addMessage, response.toXML()). Invalid XML causes Plivo replies to fail silently or with errors in Plivo logs.
Other Common Issues
Plivo Trial Account Limitations: You can only send messages to and receive messages from numbers verified in your Plivo Sandbox (Phone Numbers > Sandbox Numbers).
Plivo Platform Status: Check the Plivo Status Page if you suspect a platform-wide issue.
Delayed Messages/Replies: SMS is not guaranteed real-time. Delays can occur due to carrier network congestion.
Firewall Blocking Plivo: Ensure your production server's firewall allows incoming connections from Plivo's IP addresses on the relevant port (usually 443 for HTTPS). Consult Plivo's documentation or support for the specific IP ranges if needed for firewall configuration.
12. Deployment and CI/CD
Deploy your Node.js/Express application to a server and ensure it restarts automatically. Replace your temporary ngrok URL in Plivo with your permanent production HTTPS URL.
Choose a Hosting Platform
Platform Type
Examples
Best For
PaaS
Heroku, Render, Fly.io
Simple deployment, managed HTTPS
IaaS
AWS EC2, Google Compute Engine, DigitalOcean
Full control, custom configuration
Serverless
AWS Lambda + API Gateway
Low/variable traffic, cost efficiency
Prepare for Production
Environment Variables: Configure PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PORT on the production server (do not commit .env). Platforms like Heroku have config vars; servers use system environment variables.
Use a Process Manager: Use PM2 to manage your Node.js process, handle restarts on failure, enable clustering, and manage logs.
Basic PM2 Commands
Bash
# Install PM2 globally
npm install -g pm2
# Start your application
pm2 start server.js --name "plivo-sms-app"# Enable auto-restart on system reboot
pm2 startup
pm2 save
# Monitor your application
pm2 monit
# View logs
pm2 logs
13. Testing Strategies
14. Compliance and Legal Considerations
15. Real-World Examples and Use Cases
Conclusion
You now know how to receive SMS messages in Node.js using Plivo and Express. You've built a production-ready webhook handler with:
A secure endpoint that validates Plivo signatures
Automated SMS reply functionality based on keywords
Error handling and logging for reliability
Configuration for local development and production deployment
Next Steps
Add database integration – Store conversation history and user preferences
Implement advanced logic – Build conversation flows and state management
Deploy to production – Choose a hosting platform and configure CI/CD
Monitor and optimize – Set up APM tools and track key metrics
Ensure compliance – Review TCPA and GDPR requirements for your use case
Use the Plivo Node.js SDK and Express.js to create a webhook endpoint that Plivo can send incoming SMS messages to. The webhook receives message details like sender number, recipient number, and message content in the request body. Make sure to validate the Plivo signature to secure your webhook endpoint. This lets you process incoming messages and respond programmatically.
The Plivo Node.js SDK simplifies interaction with the Plivo API, including generating XML responses for sending SMS messages and validating Plivo webhook signatures for enhanced security. The SDK makes it easier to interact with Plivo's services from your application, like sending SMS messages, making calls, and managing other Plivo related communications from your Node.js server.
Plivo webhook validation, using signatures, is crucial for security as it verifies that incoming requests genuinely originate from Plivo, preventing spoofing or unauthorized access to your webhook. The validation process involves comparing the signature received in the headers of the request with a calculated signature and ensures only legitimate Plivo webhooks are processed by your app.
Use ngrok during development to expose your local Express.js server to the internet so Plivo can reach your webhook. ngrok creates a temporary public URL that tunnels requests to your local server. However, ngrok is not suitable for production; deploy your app to a server with a stable HTTPS URL for live applications.
You can simulate a Plivo webhook POST request using cURL, but you cannot fully replicate the Plivo signature validation this way. While you can test the basic request handling, proper testing requires sending a real SMS via Plivo or using their API console to generate valid signatures, which are necessary for verifying the request truly came from Plivo. Without a valid signature, tests won't accurately simulate a real Plivo webhook and might fail.
Use the Plivo Node.js SDK's plivo.Response object to construct an XML response containing a <Message> element. Set the src parameter to your Plivo number and the dst parameter to the sender's number. This XML instructs Plivo to send the SMS reply. This XML message tells Plivo how to handle the interaction, in this case by sending a message from your Plivo number to the recipient.
The article recommends using Node.js v14 or later for optimal compatibility with the Plivo SDK and Express framework. While older versions may work, staying updated ensures you are using the latest security fixes along with potentially better performance.
Use express.urlencoded({ extended: true }) middleware to parse incoming data. Implement a security middleware to validate the Plivo signature, ensuring the request is from Plivo. Then, extract message details (From, To, Text) from req.body, implement your logic, and send a Plivo XML response using plivo.Response to send the reply.
A suggested schema includes a conversations table with fields like conversation_id, user_phone_number, and status, and a messages table with fields like message_id, conversation_id, direction, text, and timestamp. Storing this data can provide a better understanding of how your application is performing and also context into the history of interactions with users.
Rate limiting protects your webhook from abuse, denial-of-service attacks, and exceeding Plivo's rate limits. Use middleware like express-rate-limit to limit the number of requests from a single IP or Plivo number within a time window, improving security and reliability.
Long SMS messages are split into segments. Plivo sends each segment as a separate webhook request. Use parameters like ParentMessageUUID and sequence numbers to reassemble the complete message before processing. You may need to buffer these incoming message segments and then reassemble them into one cohesive message once all the segments have been received. This prevents partial messages being processed as full ones.
Common issues include incorrect Message URL or Method in the Plivo Application settings, ngrok tunnel issues, webhook signature validation failures (often due to incorrect Auth Token or URL mismatches), or firewall blocking Plivo's IP addresses. Double-check these settings if encountering problems with integrating your Node.js application with Plivo.
While Plivo handles standard opt-out keywords for certain numbers, you must implement logic in your application to honor these requests, typically by flagging the user as unsubscribed in your database. This is crucial for regulatory compliance (e.g., TCPA). So you need to do more than just reply confirming the opt-out; your application should store the opt-out status.
Learn how to receive SMS messages in Node.js using Plivo's SMS API and Express framework. This comprehensive guide walks you through building a production-ready application that handles inbound SMS webhooks, validates signatures, and sends automated replies—perfect for customer support bots, appointment confirmations, and interactive messaging systems.
When someone sends an SMS to your Plivo phone number, Plivo forwards it to your Express application via HTTP webhook. You'll learn how to set up your project, configure the webhook endpoint, process incoming messages, generate XML responses, secure your application, and deploy it to production. By the end, you'll have a functional Express application that receives SMS messages and responds automatically.
Common Use Cases
Technologies Used
express.urlencoded()andexpress.json())..envfile.System Architecture
How It Works
/receive_smsendpointPrerequisites
Before you can receive SMS in Node.js with Plivo, you'll need:
ngrokinstalled for local development testing (Download ngrok).1. Setting Up Your Node.js Project to Receive SMS
Create a new Node.js project and install the dependencies needed to receive SMS messages with Plivo and Express.
Create Project Directory: Create a new directory for your project, then navigate into it.
mkdir plivo-node-sms-inbound cd plivo-node-sms-inboundInitialize Node.js Project: Initialize the project using npm to create a
package.jsonfile.The
-yflag accepts the default settings.Install Dependencies: Install Express for the web server, the Plivo Node.js SDK, and
dotenvfor managing environment variables. Modern Express (v4.16+) includes the necessary middleware for parsing URL-encoded bodies, sobody-parseris no longer required separately.express: The web framework. Includesexpress.urlencoded()middleware.plivo: The official Plivo Node.js SDK.dotenv: Loads environment variables from a.envfile intoprocess.env.Create Project Structure: Create the basic files and directories.
touch server.js .env .gitignoreserver.js: Contains your main application code..env: Stores sensitive credentials like your Plivo Auth Token. Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them.Set Up Environment Variables (
.env): Add your Plivo Auth ID and Auth Token for webhook validation. Find these on your Plivo Console dashboard.YOUR_PLIVO_AUTH_IDandYOUR_PLIVO_AUTH_TOKENwith your actual credentials.PORT: The port your local server will run on.2. Creating Your Express SMS Webhook Handler
Build the Express application to receive SMS webhooks from Plivo. When someone texts your Plivo number, Plivo sends an HTTP POST request to your webhook endpoint. This section shows you how to handle incoming messages and send automated replies.
Understanding How Plivo SMS Webhooks Work
A webhook is an HTTP callback that Plivo uses to notify your application about incoming messages. When Plivo receives an SMS at your number, it immediately sends an HTTP POST request to your configured Message URL. Your Express app processes the request and responds with XML instructions telling Plivo what to do next—such as sending a reply SMS back to the sender.
server.js:// server.js // 1. Load Environment Variables require('dotenv').config(); // 2. Import Dependencies const express = require('express'); const plivo = require('plivo'); const crypto = require('crypto'); // Required internally by Plivo SDK for webhook validation // 3. Initialize Express App const app = express(); const port = process.env.PORT || 3000; // Use port from .env or default to 3000 // 4. Middleware Setup // Use built-in Express middleware to parse URL-encoded bodies (which Plivo uses) app.use(express.urlencoded({ extended: true })); // Middleware for Plivo Webhook Validation (Crucial Security Step) const validatePlivoSignature = (req, res, next) => { const signature = req.header('X-Plivo-Signature-V3'); const nonce = req.header('X-Plivo-Signature-V3-Nonce'); // Construct the URL Plivo used to call the webhook. // Note: This reconstruction might be fragile behind certain reverse proxies or load balancers. // Verify this matches how Plivo sees the URL, or consult Plivo docs for alternative validation methods if issues arise. const url = req.protocol + '://' + req.get('host') + req.originalUrl; const method = req.method; // Should be POST for Plivo webhooks if (!signature || !nonce) { console.warn('Missing Plivo signature headers'); return res.status(400).send('Missing signature headers'); } try { // Use the Plivo SDK's validateV3Signature method (as of SDK v4.74.0, January 2025) // This validates using HMAC-SHA256 with your Auth Token, protecting against replay attacks const valid = plivo.validateV3Signature(method, url, nonce, process.env.PLIVO_AUTH_TOKEN, signature); if (valid) { console.log('Plivo signature validation successful.'); next(); // Signature is valid, proceed to the route handler } else { console.warn('Invalid Plivo signature.'); return res.status(403).send('Invalid signature'); // Forbidden } } catch (error) { console.error('Error during Plivo signature validation:', error); return res.status(500).send('Signature validation error'); } }; // 5. Define Webhook Route Handler // Apply the validation middleware ONLY to the Plivo webhook route app.post('/receive_sms', validatePlivoSignature, (req, res) => { // Plivo sends message details in the request body const from_number = req.body.From; const to_number = req.body.To; // Your Plivo number const text = req.body.Text; const message_uuid = req.body.MessageUUID; // Unique ID for the incoming message console.log(`Message received on ${to_number} from ${from_number}: "${text}" (UUID: ${message_uuid})`); // --- Business Logic Here --- // Example: Simple auto-reply based on keyword // Using template literals for cleaner string construction let reply_text = `Thank you for your message! We received: "${text}"`; if (text && text.toLowerCase().includes('help')) { reply_text = `Need help? Contact support@example.com or call 1-800-555-HELP.`; } else if (text && text.toLowerCase().includes('stop')) { reply_text = `You have been unsubscribed. No more messages will be sent.`; // Add logic here to actually unsubscribe the user in your system } // --- End Business Logic --- // 6. Generate Plivo XML Response // Use the Plivo SDK to create a <Response> XML object const response = new plivo.Response(); // Add a <Message> element to the response to send an SMS back const params = { src: to_number, // The reply comes FROM your Plivo number dst: from_number // The reply goes TO the sender's number }; response.addMessage(reply_text, params); // 7. Send XML Response // Convert the response object to XML and set the correct Content-Type const xmlResponse = response.toXML(); console.log(`Sending XML Response:\n`, xmlResponse); res.setHeader('Content-Type', 'application/xml'); res.status(200).send(xmlResponse); }); // Basic health check endpoint (optional but good practice) app.get('/health', (req, res) => { res.status(200).send('OK'); }); // 8. Start the Server (Only if this file is run directly) // This check allows the 'app' object to be exported for testing without starting the server if (require.main === module) { app.listen(port, () => { console.log(`Server running on port ${port}`); console.log(`Plivo Auth ID: ${process.env.PLIVO_AUTH_ID ? 'Loaded' : 'MISSING!'}`); console.log(`Plivo Auth Token: ${process.env.PLIVO_AUTH_TOKEN ? 'Loaded' : 'MISSING!'}`); console.log('Make sure ngrok is running and pointed to this port for development testing.'); console.log(`Webhook URL should be configured in Plivo as: http://<your-ngrok-url>/receive_sms`); }); } // Export the app for testing purposes module.exports = app;Code Explanation
require('dotenv').config()loads variables from.env.cryptomodule (needed internally by the SDK's validation function).express.urlencoded: Parses the incoming webhook data from Plivo using Express's built-in middleware.validatePlivoSignature: Crucial security middleware. It verifies that incoming requests genuinely originated from Plivo using theX-Plivo-Signature-V3header, a nonce, your Auth Token, and the request details. This prevents attackers from spoofing requests to your webhook. It's applied only to the/receive_smsroute. A note about potential URL reconstruction issues behind proxies is included./receive_sms):validatePlivoSignaturemiddleware.From,To,Text,MessageUUID) fromreq.body.plivo.Responseobject.response.addMessage(reply_text, params)to add a<Message>tag to the XML. This tag instructs Plivo to send an SMS reply.src(source) is your Plivo number (to_numberfrom the incoming message).dst(destination) is the original sender's number (from_number).response.toXML().Content-Typeheader toapplication/xml.server.jsis executed directly (not whenrequired by tests).appobject is exported so it can be imported by testing frameworks likesupertest.3. Building a Complete API Layer
In this specific scenario, the "API" is the single webhook endpoint (
/receive_sms) that Plivo interacts with. We've already implemented:validatePlivoSignaturemiddleware). Only requests signed correctly with your Plivo Auth Token are processed./receive_smsroute handler if needed./receive_smsPOSTapplication/x-www-form-urlencodedX-Plivo-Signature-V3,X-Plivo-Signature-V3-NonceFrom: Sender's phone number (E.164 format).To: Your Plivo number receiving the message (E.164 format).Text: The content of the SMS message.Type:smsMessageUUID: Unique identifier for the incoming message.Encoding,ParentMessageUUID, etc. – see Plivo docs)application/xml<?xml version="1.0" encoding="utf-8"?> <Response> <Message src="+14155551212" dst="+14155559876">Thank you for your message! We received: "Hello"</Message> </Response>403 Forbidden, Body:Invalid signature400 Bad Request, Body:Missing signature headersTesting with cURL (Without Valid Signature)
You can simulate a POST request, but without a valid signature, it should be rejected by your validation middleware.
curl -X POST http://localhost:3000/receive_sms \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "From=+15551234567&To=+15557654321&Text=Hello+from+curl&MessageUUID=abc-123" # Expected Response (if validation is working): # Invalid signature (or Missing signature headers) with a 403 or 400 status codeProper testing requires sending a real SMS via Plivo or using Plivo's API console/tools that generate correct signatures.
4. Configuring Plivo to Send Webhooks to Your Express App
Connect your Express application to Plivo by configuring a Message URL. During development, use
ngrokto expose your local server. Note: The ngrok free tier includes 1 static domain (available since August 2023), keeping your webhook URL consistent. For production, deploy to a server with a stable HTTPS URL.What Is a Plivo Application?
A Plivo Application tells Plivo where to forward incoming SMS messages. When someone texts your Plivo number, the Application's Message URL determines which webhook endpoint receives the data. Think of it as the routing configuration between your Plivo phone number and your Express server.
Run Your Local Server: Ensure your
.envfile is populated with your Plivo credentials.You should see output indicating the server is running on port 3000 (or your configured port).
Expose Local Server with ngrok (Development Only): Open a new terminal window and run
ngrokto create a public URL tunnel to your local server's port.ngrokdisplays output similar to this:Copy the
https://forwarding URL (e.g.,https://xxxxxxxxxxxx.ngrok.io). This is your temporary public webhook URL for Plivo during development.Node Express SMS Handler).https://ngrok forwarding URL and append your webhook path:https://xxxxxxxxxxxx.ngrok.io/receive_sms(Replace with your actual ngrok URL or production URL later).POST.XML Application.Node Express SMS Handler).Your Express application is now linked to your Plivo number. When an SMS is sent to that number, Plivo makes a POST request to your configured URL (ngrok for dev, production URL for live), which forwards it to your
server.jsapplication.Common Configuration Issues
https://and/receive_sms5. Error Handling and Logging
Your current application has basic logging and webhook validation error handling. Enhance the route handler with internal error catching.
/receive_smsroute after validation, wrap your logic in atry...catchblock.console.logprovides basic information. For production, use more robust logging libraries likeWinstonorPino, which enable structured logging, different log levels (info, warn, error), and routing logs to files or external services.Enhanced Error Handling
// Inside server.js – Refined Route Handler app.post('/receive_sms', validatePlivoSignature, (req, res) => { try { // Start try block after validation const from_number = req.body.From; const to_number = req.body.To; // Your Plivo number const text = req.body.Text; const message_uuid = req.body.MessageUUID; // Unique ID for the incoming message // Basic check for missing essential data (optional, but good practice) if (!from_number || !to_number || !message_uuid) { console.warn(`Incomplete message data received: From=${from_number}, To=${to_number}, UUID=${message_uuid}`); // Respond 200 OK to Plivo to prevent retries, but don't process further. return res.status(200).send(new plivo.Response().toXML()); } console.log(`Message received on ${to_number} from ${from_number}: "${text || '(empty)'}" (UUID: ${message_uuid})`); // --- Business Logic Here --- let reply_text = `Thank you for your message! We received: "${text || '(empty)'}"`; if (text && text.toLowerCase().includes('help')) { reply_text = `Need help? Contact support@example.com or call 1-800-555-HELP.`; } else if (text && text.toLowerCase().includes('stop')) { reply_text = `You have been unsubscribed. No more messages will be sent.`; // Add unsubscribe logic console.log(`STOP keyword received from ${from_number}. Unsubscribe logic should run.`); } // --- End Business Logic --- const response = new plivo.Response(); const params = { src: to_number, dst: from_number }; response.addMessage(reply_text, params); const xmlResponse = response.toXML(); console.log(`Sending XML Response:\n`, xmlResponse); res.setHeader('Content-Type', 'application/xml'); res.status(200).send(xmlResponse); } catch (error) { // Catch unexpected internal processing errors console.error(`Error processing incoming SMS ${req.body?.MessageUUID || 'UNKNOWN'}:`, error); // Avoid sending sensitive error details back. // Sending an empty <Response/> tells Plivo we handled it but won't reply. const errorResponse = new plivo.Response(); res.setHeader('Content-Type', 'application/xml'); // Respond 200 to Plivo to acknowledge receipt and prevent Plivo retries, // even though an internal server error occurred. Log the error for debugging. res.status(200).send(errorResponse.toXML()); // Alternatively, if you want Plivo to retry due to a transient error: // res.status(500).send('Internal Server Error'); // Use with caution regarding retries. } });Plivo Retry Behavior
Plivo automatically retries webhooks three times if it does not receive a
200 OKstatus code (as of January 2025):By responding
200 OKeven in ourcatchblock (with an empty XML response), we tell Plivo we've received the webhook and prevent retries for that specific failure. If the failure is transient and you want Plivo to retry, respond with a5xxstatus code. Be cautious, as this could lead to duplicate processing if not handled carefully.Note: Plivo offers configurable callback retry options (announced May 2024) that let you customize timeout values and retry policies by appending parameters to webhook URLs (e.g.,
#ct=2000&rt=3000for connection and read timeouts).6. Database Schema and Data Layer (Conceptual)
This simple auto-responder doesn't require a database. However, more complex interactions need one:
Use Cases: Store conversation history, user profiles, subscription status, order details, etc.
Schema Example (Conceptual – e.g., for tracking conversations):
conversationsconversation_id (PK),user_phone_number (FK),plivo_number (FK),created_at,last_updated_at,status(e.g., open, closed)messagesmessage_id (PK),conversation_id (FK),plivo_message_uuid (Unique),direction(inbound/outbound),sender_number,recipient_number,text,timestampData Access: Use an ORM like Prisma or Sequelize, or a database driver (e.g.,
pgfor PostgreSQL,mysql2for MySQL) to interact with the database within your/receive_smslogic. Typically fetch relevant conversation context based on thefrom_numberandto_numberbefore deciding on a reply.Migrations: Use the migration tools provided by your ORM (e.g.,
prisma migrate dev,sequelize db:migrate) to manage schema changes.7. Security Features
Security is paramount when exposing webhooks to the internet.
Webhook Signature Validation: Already implemented (
validatePlivoSignature). This is the most critical security measure. It ensures requests are authentic and originated from Plivo.Input Sanitization: While Plivo handles SMS encoding, if you use the
Textinput in database queries or complex dynamic responses, sanitize it to prevent injection attacks (SQL injection, XSS). Libraries likevalidator.jscan help. For basic replies like this example, the risk is lower.Rate Limiting: Protect your endpoint from abuse or denial-of-service attacks by limiting the number of requests from a single IP or for a specific Plivo number. Use middleware like
express-rate-limit.// server.js (add near other middleware, before routes) const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests, please try again later.', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the rate limiting middleware to the SMS webhook route app.use('/receive_sms', limiter);HTTPS: Always use HTTPS for your webhook URL in production.
ngrokprovides this automatically for development. In production, ensure your server is configured behind a reverse proxy (like Nginx or Apache) or load balancer that handles SSL/TLS termination, or use a PaaS that provides it. Plivo requires HTTPS for production webhooks.Environment Variables: Keep sensitive information (Plivo Auth Token, database credentials) out of your code and use environment variables (
.envlocally, system environment variables in production). Ensure.envis in.gitignore.Common Vulnerabilities: Be aware of OWASP Top 10 vulnerabilities and how they might apply, especially if you add database interactions or more complex logic.
Security Checklist
.envfile in.gitignore8. Handling Special Cases
Message Encoding: Plivo handles standard SMS encodings (GSM-7, UCS-2). The
Textfield in the webhook payload contains the decoded message content. Be aware of character limits (160 for GSM-7, 70 for UCS-2 per segment).Concatenated Messages (Long SMS): Longer messages are split into multiple segments by carriers. Plivo usually forwards these as separate webhook requests. To reassemble them, look for parameters like
ParentMessageUUID(identifies the original message) and potentially sequence number parameters in the webhook payload. Buffer these parts (e.g., in memory, cache, or database) until all segments arrive before processing the full message. Consult the Plivo documentation for the exact parameters and recommended handling logic for message concatenation.Time Zones: Timestamps from Plivo are in UTC. Store timestamps in your database in UTC and convert to the user's local time zone for display if necessary.
"STOP" / Opt-Out: Plivo handles standard opt-out keywords (
STOP,UNSUBSCRIBE, etc.) at the carrier level for Toll-Free and Short Code numbers if enabled in your Plivo number's configuration. However, you must also implement logic in your application to honor these requests (e.g., flagging the user as unsubscribed in your database) to comply with regulations (like TCPA in the US). Our example includes a basic keyword check but requires adding the actual database/system update logic.Rate Limits / Throttling: Plivo enforces rate limits on sending messages. Your application might also be throttled by Plivo if it responds too slowly or incorrectly too often. Ensure your webhook responds quickly (ideally under 1–2 seconds; Plivo timeout is 5 seconds).
Duplicate Messages: Network issues can occasionally cause Plivo to send duplicate webhooks (especially if your server doesn't respond promptly). Use the
MessageUUIDto deduplicate messages by checking if you've already processed that UUID in your database or a cache (like Redis) within a short time window before processing.Character Encoding Limits
9. Performance Optimizations
For this simple application, performance is less critical, but consider these for scaling:
Fast Webhook Response: The most crucial optimization. Do minimal work synchronously within the webhook handler. If extensive processing is needed (calling external APIs, complex database queries), acknowledge the webhook quickly (send the XML response) and perform the heavy lifting asynchronously using a job queue (e.g., BullMQ, Kue with Redis) or background process.
Caching: Cache frequently accessed data (e.g., user profiles, common replies) using Redis or Memcached to reduce database load.
Efficient Database Queries: Index database columns used in lookups (e.g.,
user_phone_number,plivo_message_uuid).Load Testing: Use tools like
k6,Artillery, orApacheBench (ab)to simulate high traffic to your webhook endpoint and identify bottlenecks.Node.js Clustering: Use Node.js's built-in
clustermodule or a process manager likePM2in cluster mode to run multiple instances of your application across CPU cores, improving throughput.Performance Targets
10. Monitoring, Observability, and Analytics
Health Checks: The
/healthendpoint provides a basic check. Monitoring services (like UptimeRobot, Pingdom, AWS CloudWatch) can ping this endpoint to ensure your application is running.Performance Metrics (APM): Use Application Performance Monitoring tools (e.g., Datadog, New Relic, Dynatrace, Sentry APM) to track response times, error rates, resource usage (CPU, memory), and Node.js-specific metrics (event loop lag). These tools often auto-instrument Express applications.
Error Tracking: Integrate services like Sentry or Bugsnag to capture, aggregate, and alert on application errors (going beyond basic console logging).
Logging Aggregation: Ship logs (using Winston/Pino transports) to a centralized logging platform (e.g., ELK stack, Datadog Logs, Logz.io, Sematext) for easier searching, analysis, and alerting based on log patterns.
Key Metrics Dashboard: Create dashboards (in your APM, logging tool, or Grafana) showing:
Alerting: Configure alerts based on thresholds (e.g., error rate > 1%, response time > 2 seconds, health check fails).
Essential Metrics to Track
11. Troubleshooting and Caveats
ngrok Issues (Development)
ngrokis running and pointing to the correct local port (3000in this example).ngroktraffic.Plivo Configuration Errors
/receive_smspath is included.POSTin the Plivo Application.Webhook Signature Validation Failures
PLIVO_AUTH_TOKENin your.envfile or production environment variables exactly matches the one in the Plivo console.ngroklogs, Plivo debug logs, or your server access logs). Proxies can sometimes alter URLs/host headers.plivo.validateV3Signature.Code Errors
node server.jsoutput or aggregated logs) for runtime errors.npm install).response.addMessage,response.toXML()). Invalid XML causes Plivo replies to fail silently or with errors in Plivo logs.Other Common Issues
12. Deployment and CI/CD
Deploy your Node.js/Express application to a server and ensure it restarts automatically. Replace your temporary
ngrokURL in Plivo with your permanent production HTTPS URL.Choose a Hosting Platform
Prepare for Production
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPORTon the production server (do not commit.env). Platforms like Heroku have config vars; servers use system environment variables.PM2to manage your Node.js process, handle restarts on failure, enable clustering, and manage logs.Basic PM2 Commands
# Install PM2 globally npm install -g pm2 # Start your application pm2 start server.js --name "plivo-sms-app" # Enable auto-restart on system reboot pm2 startup pm2 save # Monitor your application pm2 monit # View logs pm2 logs13. Testing Strategies
14. Compliance and Legal Considerations
15. Real-World Examples and Use Cases
Conclusion
You now know how to receive SMS messages in Node.js using Plivo and Express. You've built a production-ready webhook handler with:
Next Steps
Related Resources