How to Send SMS with Plivo Node.js & Express: Complete Tutorial (2025)
A step-by-step guide to building a Node.js Express application for sending SMS messages using the Plivo API, covering setup, implementation, security, and testing.
Learn how to send SMS messages programmatically using Node.js, Express.js, and the Plivo messaging API. This comprehensive tutorial walks you through building a production-ready SMS application from scratch, covering authentication, error handling, security, deployment, and 10DLC compliance for US messaging.
By the end of this guide, you'll have a fully functional REST API endpoint that can send text messages to any phone number worldwide using Plivo's cloud communications platform. Perfect for implementing two-factor authentication (2FA), transactional notifications, appointment reminders, or marketing campaigns.
Framework Compatibility Note (2025): This guide supports both Express 4.x and Express 5.x. Express 5.1.0 became the default on npm as of March 31, 2025, and requires Node.js 18 or higher. Express 5 includes breaking changes: req.param() is removed (use req.params, req.body, or req.query explicitly), and promise rejections automatically forward to error-handling middleware. If using Express 5, review the Express 5 migration guide.
What You'll Build: SMS API Project Overview
Goal: Create a secure and robust Node.js Express API endpoint (POST /send-sms) that accepts a recipient phone number and message body, then uses the Plivo API to send the SMS.
Problem Solved: Provides a backend service to programmatically send SMS messages, abstracting direct Plivo API interaction behind a simple web service.
Real-World Use Cases:
Two-factor authentication (2FA) and one-time passwords (OTP)
Order confirmations and shipping notifications for e-commerce
Appointment reminders for healthcare and service businesses
Emergency alerts and critical system notifications
Marketing campaigns and promotional messages
Technologies:
Technology
Purpose
Version Notes (2025)
Node.js
JavaScript runtime for server-side applications. Asynchronous, event-driven architecture suits I/O operations like API calls.
v22 (LTS, active until Oct 2027) or v20 (LTS, maintenance until Apr 2026)
Express.js
Minimal web application framework for routes and HTTP request handling.
v5.1.0 is default (as of Mar 31, 2025); v4.x in maintenance mode
Plivo
Cloud communications platform providing SMS and Voice APIs.
Current API version
Plivo Node.js SDK
Simplifies interaction with the Plivo REST API.
Latest: ^4.x
dotenv
Loads environment variables from .env file into process.env.
Plivo Auth ID and Auth Token: Available on your Plivo account dashboard.
Plivo Phone Number or Registered Sender ID:
US/Canada: Purchase an SMS-enabled Plivo phone number. For US: As of June 1, 2023, register your traffic for 10DLC (10-Digit Long Code) compliance to avoid carrier surcharges. Register your brand and campaign via Plivo Console → Messaging → 10DLC. Unregistered traffic incurs significant surcharges.
Other Countries: Use a registered Alphanumeric Sender ID (check Plivo's country-specific guidelines and register via Plivo support) or a purchased Plivo number.
Basic JavaScript and REST API knowledge.
(Optional) curl or Postman: For testing the API endpoint.
10DLC Registration Process (US Only):
Register your brand (business entity) in Plivo Console → Messaging → 10DLC
Create a campaign describing your use case (transactional, marketing, etc.)
Wait for approval (typically 1–3 business days)
Associate your Plivo phone number with the approved campaign
Begin sending compliant traffic
Common 10DLC Issues:
High carrier surcharges despite registration: Verify campaign is approved and number is properly associated.
Low throughput limits: Consider using a Toll-Free number or Short Code for higher volume.
Trial Account Limitations: Plivo trial accounts can typically only send messages to phone numbers verified within your Plivo console (Sandbox Numbers).
SMS Pricing (US, 2025): Plivo charges $0.0055 per outbound SMS segment via long code, plus carrier surcharges ranging from $0.0030 (AT&T, T-Mobile) to $0.0050 (US Cellular, other carriers) per SMS for 10DLC-registered traffic. Check Plivo's SMS pricing page for current rates and international pricing.
Cost Calculation Examples:
Single SMS (160 characters or less): $0.0055 + $0.0030 = $0.0085 per message (AT&T/T-Mobile)
Set up Basic Express Server (server.js):
Open server.js and add the initial Express setup.
JavaScript
// server.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const app = express();
constPORT = process.env.PORT || 3000; // Use port from env var or default to 3000// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware to parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// Basic root route (optional)
app.get('/', (req, res) => {
res.send('Plivo SMS Sender API is running!');
});
// Placeholder for SMS routes (we'll add this soon)// const smsRoutes = require('./routes/sms');// app.use('/api', smsRoutes);// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
require('dotenv').config();: Loads variables from .env. Call this early.
express.json() and express.urlencoded(): Middleware needed to parse incoming request bodies.
process.env.PORT || 3000: Allows configuring the port via environment variables, essential for deployment platforms.
2. How to Send SMS with Plivo API: Implementing Core Functionality
Create the logic to interact with the Plivo API using their Node.js SDK.
Create SMS Route File:
Inside the routes directory, create a file named sms.js.
Bash
touch routes/sms.js
Implement the Sending Logic (routes/sms.js):
This file defines the router and the handler for your /send-sms endpoint.
JavaScript
// routes/sms.jsconst express = require('express');
const plivo = require('plivo');
const router = express.Router();
// Validate environment variablesconstAUTH_ID = process.env.PLIVO_AUTH_ID;
constAUTH_TOKEN = process.env.PLIVO_AUTH_TOKEN;
constSENDER_NUMBER = process.env.PLIVO_SENDER_NUMBER; // Your Plivo number or Sender IDif (!AUTH_ID || !AUTH_TOKEN || !SENDER_NUMBER) {
console.error('Error: Plivo credentials or sender number not configured in .env file.');
// In a real app, you might prevent the server from starting or handle this more gracefully.// For this guide, we'll log the error and let requests fail if credentials aren't set.
}
// Initialize Plivo client (only if credentials exist)let client;
if (AUTH_ID && AUTH_TOKEN) {
client = new plivo.Client(AUTH_ID, AUTH_TOKEN);
}
// POST /api/send-sms
router.post('/send-sms', async (req, res) => {
// Basic input validationconst { to, text } = req.body;
if (!to || !text) {
return res.status(400).json({ success: false, error: 'Missing required fields: "to" and "text".' });
}
// Basic E.164 format check (simple version)// A more robust validation library (like libphonenumber-js) is recommended for production,// as it handles varying international lengths, country codes, and number types more accurately.if (!/^\+\d{1,15}$/.test(to)) {
return res.status(400).json({
success: false,
error: 'Invalid "to" number format. Use E.164 format (e.g., +14155552671).'
});
}
if (!client) {
console.error('Plivo client not initialized due to missing credentials.');
return res.status(500).json({ success: false, error: 'Server configuration error: Plivo client not available.' });
}
try {
console.log(`Attempting to send SMS to ${to} from ${SENDER_NUMBER}`);
const response = await client.messages.create({
src: SENDER_NUMBER, // Sender ID or Plivo Number from .envdst: to, // Destination number from request bodytext: text, // Message text from request body// Optional parameters:// type: 'sms', // Message type (sms or mms)// url: 'https://your-callback.com/dlr', // Delivery receipt callback URL// method: 'POST', // Callback method// log: true, // Enable logging
});
console.log('Plivo API Response:', response);
// Plivo API generally returns a 202 Accepted on success// The response contains message UUIDs
res.status(202).json({
success: true,
message: 'SMS send request accepted by Plivo.',
plivoResponse: response,
});
} catch (error) {
console.error('Error sending SMS via Plivo:', error);
// Provide a more specific error message if possibleconst errorMessage = error.message || 'Failed to send SMS.';
const statusCode = error.statusCode || 500; // Use Plivo's status code if available
res.status(statusCode).json({
success: false,
error: 'Plivo API Error',
details: errorMessage,
plivoError: error // Include the raw error for debugging if appropriate
});
}
});
module.exports = router; // Export the router
Import express and plivo.
Create an express.Router().
Load Plivo credentials and the sender number from process.env. Add checks to ensure they exist.
Initialize the Plivo client only if credentials are present.
The POST /send-sms handler uses async/await for cleaner asynchronous code.
Input Validation: Basic checks ensure to and text are provided and to roughly matches E.164 format. A note about using better libraries like libphonenumber-js is included.
Plivo Call:client.messages.create() sends the SMS. Key parameters: src (sender), dst (recipient), text (message content). Optional parameters include type, url (delivery receipt callback), method, and log.
Response Handling: On success (Plivo returns 202 Accepted), send a success JSON response. On error, catch it, log it, and send an appropriate error JSON response including details from the Plivo error object.
Mount the Router in server.js:
Uncomment and add the lines in server.js to use the router you just created.
JavaScript
// server.jsrequire('dotenv').config();
const express = require('express');
const smsRoutes = require('./routes/sms'); // Import the routerconst app = express();
constPORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.send('Plivo SMS Sender API is running!');
});
// Mount the SMS routes under the /api path
app.use('/api', smsRoutes); // Use the imported router
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Now, requests to POST /api/send-sms will be handled by your routes/sms.js logic.
3. Building and Testing Your SMS API Endpoint
You've already implemented the core API endpoint (POST /api/send-sms) in the previous step. This section details its usage and provides testing examples.
API Endpoint Specification:
Property
Value
Method
POST
Path
/api/send-sms
Content-Type
application/json
Authentication
None (assumes internal or infrastructure-protected)
Request Body Parameters:
Parameter
Type
Required
Description
Example
to
string
Yes
Recipient's phone number in E.164 format
+14155552671
text
string
Yes
SMS message content
Hello from Plivo!
Response Status Codes:
Status Code
Meaning
When It Occurs
202 Accepted
SMS queued successfully
Valid request, Plivo accepted the message
400 Bad Request
Invalid input
Missing fields, invalid phone format
500 Internal Server Error
Server or Plivo API error
Configuration issues, Plivo service errors
Example curl Request:
Replace placeholders with your actual server address, recipient number, and message.
Bash
curl -X POST http://localhost:3000/api/send-sms \
-H "Content-Type: application/json" \
-d '{
"to": "+14155552671",
"text": "Hello from your Node.js Plivo App!"
}'
Example Success Response (HTTP 202 Accepted):
JSON
{"success":true,"message":"SMS send request accepted by Plivo.","plivoResponse":{"message":"message(s) queued","messageUuid":["1f7a77ca-f1b7-11ee-9d7c-0242ac110003"],"apiId":"1f7a6efc-f1b7-11ee-9d7c-0242ac110003"}}
Example Error Response (HTTP 400 Bad Request – Missing Field):
JSON
{"success":false,"error":"Missing required fields: \"to\" and \"text\"."}
Example Error Response (HTTP 400 Bad Request – Invalid Number Format):
JSON
{"success":false,"error":"Invalid \"to\" number format. Use E.164 format (e.g., +14155552671)."}
Example Error Response (HTTP 500 Internal Server Error – Plivo API Failure):
JSON
{"success":false,"error":"Plivo API Error","details":"Resource not found","plivoError":{/* Full Plivo error object */}}
4. Plivo Authentication: Configuring Your API Credentials
Proper configuration and secure handling of credentials are critical.
Auth ID & Auth Token: Your Auth ID and Auth Token appear on the main dashboard (landing page after login).
Sender Number/ID:
For US/Canada: Go to Phone Numbers → Buy Numbers. Search for and purchase an SMS-enabled number in the desired country/region. Note the full number in E.164 format (e.g., +12025551234).
For other regions: Check Plivo's documentation for sender ID requirements. You might use a purchased Plivo number or register an Alphanumeric Sender ID via Messaging → Sender IDs → Add Sender ID (often requires approval).
Configure Environment Variables (.env file):
Open the .env file in your project's root directory and add your credentials. Replace the placeholder values.
.env
# .env - Plivo Configuration
# DO NOT COMMIT THIS FILE TO VERSION CONTROL
# Plivo API Credentials (from Plivo Console Dashboard)
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID_HERE
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN_HERE
# Plivo Sender Number or Registered Sender ID
# For US/Canada, use the E.164 format number you purchased (e.g., +12025551234)
# For other regions, use your registered Alphanumeric Sender ID or a Plivo number
PLIVO_SENDER_NUMBER=YOUR_PLIVO_NUMBER_OR_SENDER_ID
PLIVO_AUTH_ID: Your unique authentication identifier.
PLIVO_AUTH_TOKEN: Your secret authentication token. Treat this like a password.
PLIVO_SENDER_NUMBER: The number or ID messages will appear to come from. Must be a valid Plivo number (for US/Canada) or a registered Sender ID where applicable.
Load Environment Variables:
The require('dotenv').config(); line at the top of server.js loads these variables into process.env, making them accessible throughout your application (as seen in routes/sms.js).
Security: The .env file keeps your sensitive credentials out of your source code. Ensure your .gitignore file includes .env to prevent accidentally committing it. In production environments, set these variables directly in the deployment platform's configuration settings, not via a .env file.
Platform-Specific Configuration:
AWS: Use AWS Systems Manager Parameter Store or Secrets Manager
Heroku: Configure via Heroku Config Vars (heroku config:set PLIVO_AUTH_ID=xxx)
Azure: Use Azure Key Vault or App Service Configuration
Docker: Pass environment variables via -e flag or docker-compose.yml
Fallback Mechanisms: For this simple guide, there's no fallback. In a production system requiring high availability, consider:
Implementing retries (see next section).
Having a secondary messaging provider configured and switching if Plivo experiences an outage (requires more complex logic).
5. Error Handling Best Practices for SMS Applications
Robust error handling and logging are crucial for debugging and reliability.
Error Handling Strategy:
Validation Errors: Return HTTP 400 Bad Request for invalid client input (missing fields, bad number format). Provide clear JSON error messages.
Configuration Errors: If Plivo credentials aren't set, log an error on startup and return HTTP 500 if an attempt occurs to use the uninitialized client.
Plivo API Errors: Catch errors from the client.messages.create() call. Log the detailed error. Return an appropriate HTTP status code (often 500 Internal Server Error, or use error.statusCode if Plivo's SDK provides it) and a JSON error message including details from the Plivo error.
Unexpected Errors: Use a global error handler in Express or ensure all async routes have try...catch blocks to prevent unhandled promise rejections from crashing the server.
Implement Global Error Handler:
Add this middleware at the end of server.js, after all routes:
JavaScript
// Global error handler (add after all routes in server.js)
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(err.statusCode || 500).json({
success: false,
error: err.message || 'Internal server error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
// Handle 404 errors
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Endpoint not found'
});
});
Logging:
Current: We use console.log for successful operations and Plivo responses, and console.error for errors. This is suitable for development.
Production: Use a dedicated logging library like Winston or Pino. These offer:
Different log levels (debug, info, warn, error).
Structured logging (JSON format is common for easier parsing).
Multiple transports (log to console, files, external services).
const logger = require('./logger');
logger.info('Attempting to send SMS', { to, from: SENDER_NUMBER });
logger.error('Plivo API error', { error: error.message, statusCode: error.statusCode });
Retry Mechanisms:
Sending an SMS is often idempotent from Plivo's side (sending the same request usually doesn't result in duplicate messages if the first was accepted), but network issues could prevent the request from reaching Plivo.
For critical messages, implement a simple retry strategy with exponential backoff for network errors or specific transient Plivo errors (e.g., 5xx status codes). Libraries like async-retry can help.
Caution: Don't retry indefinitely or for errors that won't resolve (like invalid credentials or invalid recipient numbers).
Common Plivo Error Codes:
Error Code
Description
Recommended Action
400
Bad Request – invalid parameters
Fix request format, don't retry
401
Unauthorized – invalid credentials
Check Auth ID/Token, don't retry
404
Resource not found
Verify phone number/sender ID, don't retry
429
Too many requests – rate limit exceeded
Implement backoff, retry after delay
500/502/503
Server errors
Retry with exponential backoff
Testing Error Scenarios:
Send requests missing to or text.
Send requests with invalid to number formats.
Temporarily modify .env with incorrect PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN to test authentication failures.
Temporarily modify PLIVO_SENDER_NUMBER to an invalid one.
(If possible) Test with a recipient number known to cause issues (e.g., landline, blocked number).
Simulate network issues if testing infrastructure allows.
6. Database Schema and Data Layer
For this specific guide (only sending an SMS via an API call), a database is not strictly required.
However, in a real-world application, add a database (like PostgreSQL, MySQL, MongoDB) to:
Log Sent Messages: Store details like messageUuid, recipient, sender, timestamp, status (initially "queued", potentially updated via webhooks), and message content. This aids auditing and tracking.
Manage Users/Contacts: Store recipient information if sending to registered users.
Queue Messages: Store messages to be sent asynchronously by a background worker, especially for bulk sending or to decouple the API response from the actual sending process.
Example Database Schema (PostgreSQL):
SQL
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
plivo_message_uuid VARCHAR(255) UNIQUENOT NULL,
recipient_number VARCHAR(20) NOT NULL,
sender_id VARCHAR(20) NOT NULL,
message_text TEXT NOT NULL,
status VARCHAR(50) DEFAULT'queued',
error_message TEXT,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
delivered_at TIMESTAMP
);
CREATE INDEX idx_messages_status ON messages(status);
CREATE INDEX idx_messages_recipient ON messages(recipient_number);
CREATE INDEX idx_messages_created_at ON messages(created_at DESC);
Field Descriptions:
Field
Type
Purpose
id
SERIAL
Primary key
plivo_message_uuid
VARCHAR(255)
Unique identifier from Plivo
recipient_number
VARCHAR(20)
E.164 formatted phone number
sender_id
VARCHAR(20)
Sender number or alphanumeric ID
message_text
TEXT
Message content
status
VARCHAR(50)
Message status (queued, sent, delivered, failed)
error_message
TEXT
Error details if message failed
created_at
TIMESTAMP
When message was created
updated_at
TIMESTAMP
Last status update
delivered_at
TIMESTAMP
When message was delivered
(Full implementation details omitted as they are beyond the scope of this basic SMS sending guide).
7. Security Best Practices for SMS APIs
Enhance the security of your API endpoint.
Input Validation and Sanitization:
Validation: We already implemented basic validation for to and text. For production, use a robust validation library like joi or express-validator.
Example with Joi:
JavaScript
constJoi = require('joi');
const smsSchema = Joi.object({
to: Joi.string()
.pattern(/^\+\d{1,15}$/)
.required()
.messages({
'string.pattern.base': 'Phone number must be in E.164 format (e.g., +14155552671)',
'any.required': 'Recipient phone number is required'
}),
text: Joi.string()
.min(1)
.max(1600)
.required()
.messages({
'string.max': 'Message text must not exceed 1600 characters',
'any.required': 'Message text is required'
})
});
// In route handler:const { error, value } = smsSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
error: error.details[0].message
});
}
Common Vulnerabilities & Protections:
Authentication/Authorization: This guide omits direct API authentication, assuming protection via infrastructure. For many real-world uses, especially if the API isn't purely internal, implement authentication. Simple methods include checking for a secret API key in headers (X-API-Key) or using more robust methods like JWT.
Rate Limiting: Protect against brute-force attacks or abuse where users flood the API with requests.
Security Headers: Use headers like X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security, X-Frame-Options to mitigate common web vulnerabilities.
Implement Rate Limiting:
Use a library like express-rate-limit.
Install:npm install express-rate-limit
Apply in server.js:
JavaScript
// server.jsrequire('dotenv').config();
const express = require('express');
const rateLimit = require('express-rate-limit'); // Importconst smsRoutes = require('./routes/sms');
const app = express();
constPORT = process.env.PORT || 3000;
// Apply rate limiting to API routesconst apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: '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
});
app.use('/api', apiLimiter); // Apply the limiter to /api routes
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic root route
app.get('/', (req, res) => {
res.send('Plivo SMS Sender API is running!');
});
app.use('/api', smsRoutes);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Adjust windowMs and max according to your expected usage and security requirements.
Implement Basic Security Headers:
Use the helmet middleware.
Install:npm install helmet
Apply in server.js:
JavaScript
// server.jsrequire('dotenv').config();
const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet'); // Importconst smsRoutes = require('./routes/sms');
const app = express();
constPORT = process.env.PORT || 3000;
app.use(helmet()); // Use Helmet for basic security headersconst apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowMsmessage: '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
});
app.use('/api', apiLimiter);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic root route
app.get('/', (req, res) => {
res.send('Plivo SMS Sender API is running!');
});
app.use('/api', smsRoutes);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
helmet() applies several default security-related HTTP headers.
Protecting API Keys: Already covered by using .env and .gitignore. Ensure production environments use secure configuration management.
Testing for Vulnerabilities:
Use security scanning tools (like OWASP ZAP, Burp Suite) against your deployed application (in a test environment).
Perform manual penetration testing focusing on input validation, authentication/authorization (if added), and potential injection points.
8. Understanding SMS Message Format and Compliance
E.164 Format: Strictly enforce the + prefix and country code format for to numbers. Plivo requires this. Consider libphonenumber-js for robust parsing and validation, as it understands nuances like variable number lengths per country.
Plivo handles encoding automatically based on the text content. Longer messages or messages with Unicode characters split into multiple segments (concatenated on the recipient's device) and billed accordingly. See Plivo's Encoding and Concatenation guide.
Message Length Examples:
Standard SMS (GSM): "Hello, your code is 123456" = 28 chars = 1 segment
Unicode SMS: "Hello 👋 your code is 123456" = 30 chars = 1 segment (70 char limit)
Long SMS (GSM): 320 characters = 3 segments (153 chars each due to concatenation headers)
Sender ID Rules: US/Canada requires a Plivo number. Other countries have varying rules for numeric vs. alphanumeric sender IDs, often requiring pre-registration. Using an invalid or unregistered sender ID will cause message delivery failure. Check Plivo's country-specific SMS guidelines.
Trial Account Limitations: Sending is restricted to verified "Sandbox" numbers.
Opt-Out Handling: Regulations (like TCPA in the US) require handling STOP and HELP keywords. Plivo can manage standard opt-outs automatically for long codes and Toll-Free numbers if you configure this in the console. Ensure compliance with local regulations.
Compliance Requirements by Region:
US (TCPA): Obtain prior express written consent, honor opt-outs, register for 10DLC
EU (GDPR): Obtain explicit consent, maintain opt-out records, provide data access
Canada (CASL): Document consent, include contact info, honor unsubscribe requests
Australia (SPAM Act): Obtain consent, identify sender, provide opt-out mechanism
9. Performance Optimization for High-Volume SMS
For this simple single-message API, major optimizations are usually unnecessary. However, for higher volume:
Asynchronous Operations: Node.js and the Plivo SDK are already asynchronous, preventing the server from blocking during the API call. Using async/await maintains this benefit with cleaner syntax.
Connection Pooling: The Plivo SDK likely handles underlying HTTP connection management. No specific action needed here.
Queuing for High Volume: If you need to send many messages quickly (e.g., bulk notifications), don't call the Plivo API directly within the HTTP request handler for each message. Instead:
Accept the request quickly.
Push the message details (to, text) onto a message queue (like RabbitMQ, Redis Streams, AWS SQS, BullMQ).
Have separate background worker processes read from the queue and make the calls to the Plivo API. This decouples the API from the sending process, improves response times, and allows for better rate control and retries.
Caching: Not applicable for sending unique messages. Could be relevant if fetching contact info or templates frequently.
Load Testing: Use tools like k6, Artillery, or ApacheBench (ab) to simulate concurrent users and identify bottlenecks under load. Monitor CPU, memory, and response times during tests.
Profiling: Use Node.js built-in profiler (node --prof) or tools like Clinic.js to identify performance hotspots in your code.
10. Monitoring and Observability for SMS Applications
For production readiness:
Health Checks: Add a simple health check endpoint that verifies basic application status.
JavaScript
// Add to server.js
app.get('/health', (req, res) => {
// Basic check: just confirm the server is running// More advanced: check DB connection, Plivo connectivity
res.status(200).json({ status: 'UP', timestamp: newDate().toISOString() });
});
Performance Metrics: Monitor key metrics like request latency, request rate (RPM), error rates (4xx, 5xx), and resource usage (CPU, memory). Tools like Prometheus/Grafana, Datadog APM, or New Relic can track these.
Error Tracking: Integrate an error tracking service like Sentry or Bugsnag.
Complete Sentry Integration:
JavaScript
// Install: npm install @sentry/node @sentry/tracing// At the top of server.js, before other importsconstSentry = require('@sentry/node');
constTracing = require('@sentry/tracing');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0,
integrations: [
newSentry.Integrations.Http({ tracing: true }),
newTracing.Integrations.Express({ app }),
],
});
// Request handler must be first middleware
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// All your routes go here// Error handler must be before any other error middleware
app.use(Sentry.Handlers.errorHandler());
Logging Best Practices: For production, replace console.log with a structured logging library (Winston, Pino) that supports log levels, JSON formatting, and multiple transports (console, files, external services).
Application Monitoring: Use APM tools to track request flows, database queries, external API calls, and identify performance bottlenecks.
Analytics: Track SMS metrics like total sent, delivery success rate, failure reasons, and message costs to understand usage patterns and optimize your implementation.
Deploying Your SMS Application to Production
Environment Configuration: Use environment-specific .env files or platform configuration (Heroku Config Vars, AWS Parameter Store, etc.) to manage credentials and settings across development, staging, and production.
Process Management: Use a process manager like PM2 or systemd to keep your Node.js application running, handle crashes with automatic restarts, and manage multiple instances for load balancing.
Load Balancing: Deploy multiple instances of your application behind a load balancer (NGINX, AWS ALB, etc.) to distribute traffic and ensure high availability.
SSL/TLS: Always use HTTPS in production. Obtain SSL certificates from Let's Encrypt or your cloud provider.
Security Hardening: Review OWASP Top 10 vulnerabilities, implement proper input validation, use security headers (helmet), enable rate limiting, and keep dependencies updated.
Monitoring and Alerting: Set up alerts for critical metrics like high error rates, slow response times, or service downtime. Use tools like PagerDuty, Opsgenie, or cloud provider alerting.
Backup and Disaster Recovery: If you add a database, implement regular backups and test your disaster recovery procedures.
Cost Optimization: Monitor your Plivo usage and costs. Implement message queuing for batch sends to optimize throughput. Consider caching frequently accessed data to reduce API calls.
This comprehensive guide provides everything you need to send SMS messages with Plivo, Node.js, and Express. Build upon these patterns to create more sophisticated messaging features tailored to your application's needs.
Frequently asked questions
Use the Plivo API and Node.js SDK within an Express app. Create a POST /send-sms endpoint that accepts recipient number and message text, then uses the Plivo SDK to send the SMS via your Plivo account.
Plivo is a cloud communications platform that provides the SMS sending functionality. The Node.js app interacts with Plivo's API using the Plivo Node.js SDK, abstracting away low-level API details.
Express.js simplifies the process of setting up routes and handling HTTP requests in the Node.js application. It provides a straightforward way to create the /send-sms API endpoint.
For sending SMS in the US or Canada, you must purchase a Plivo phone number. For other countries, you might be able to use a registered Alphanumeric Sender ID, but check Plivo's guidelines as requirements vary by country.
Trial accounts typically restrict sending to verified "Sandbox" numbers within your Plivo console. You'll need a full Plivo account to send to regular numbers.
Initialize a Node.js project using npm init, then install the 'express', 'plivo', and 'dotenv' packages. Create server.js for the main application logic, and a routes/sms.js file for the API endpoint handler.
The .env file stores sensitive credentials like your Plivo Auth ID, Auth Token, and Sender Number. The 'dotenv' package loads these into process.env, making them accessible to your application.
Implement error handling for invalid inputs, missing credentials, and Plivo API errors. Use try...catch blocks to manage Plivo API calls, logging errors and sending appropriate responses to the client.
Use the E.164 international number format, which includes a '+' prefix followed by the country code and phone number (e.g., +12025550100).
Use environment variables (.env) for credentials. Implement robust input validation and consider rate limiting using a library like 'express-rate-limit' and security headers with 'helmet'.
The example code sends to one recipient per request. For multiple recipients, loop through the recipient list in your route handler and call client.messages.create for each, or use Plivo's bulk messaging API.
Check the error details in the Plivo API response and logs. Retry the request for transient errors (e.g. network or 5xx Plivo errors) using a retry mechanism with exponential backoff.
Log incoming request details, attempts to send SMS, Plivo responses (including 'messageUuid'), and detailed errors for debugging and auditing purposes. Use a logging library for production.
Use 'curl' or Postman to send requests to your local server or deployed API. Test both valid requests and error scenarios, including missing inputs, bad formats, and invalid credentials.
Learn how to send SMS messages programmatically using Node.js, Express.js, and the Plivo messaging API. This comprehensive tutorial walks you through building a production-ready SMS application from scratch, covering authentication, error handling, security, deployment, and 10DLC compliance for US messaging.
By the end of this guide, you'll have a fully functional REST API endpoint that can send text messages to any phone number worldwide using Plivo's cloud communications platform. Perfect for implementing two-factor authentication (2FA), transactional notifications, appointment reminders, or marketing campaigns.
Framework Compatibility Note (2025): This guide supports both Express 4.x and Express 5.x. Express 5.1.0 became the default on npm as of March 31, 2025, and requires Node.js 18 or higher. Express 5 includes breaking changes:
req.param()is removed (usereq.params,req.body, orreq.queryexplicitly), and promise rejections automatically forward to error-handling middleware. If using Express 5, review the Express 5 migration guide.What You'll Build: SMS API Project Overview
Goal: Create a secure and robust Node.js Express API endpoint (
POST /send-sms) that accepts a recipient phone number and message body, then uses the Plivo API to send the SMS.Problem Solved: Provides a backend service to programmatically send SMS messages, abstracting direct Plivo API interaction behind a simple web service.
Real-World Use Cases:
Technologies:
^4.x.envfile intoprocess.env.^16.0.0or laterSystem Architecture:
Prerequisites:
curlor Postman: For testing the API endpoint.10DLC Registration Process (US Only):
Common 10DLC Issues:
Trial Account Limitations: Plivo trial accounts can typically only send messages to phone numbers verified within your Plivo console (Sandbox Numbers).
SMS Pricing (US, 2025): Plivo charges $0.0055 per outbound SMS segment via long code, plus carrier surcharges ranging from $0.0030 (AT&T, T-Mobile) to $0.0050 (US Cellular, other carriers) per SMS for 10DLC-registered traffic. Check Plivo's SMS pricing page for current rates and international pricing.
Cost Calculation Examples:
1. Setting up Your Node.js Express Project
Initialize your Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir plivo-node-sms-guide cd plivo-node-sms-guideInitialize Node.js Project: Create a
package.jsonfile to manage project dependencies and scripts.(The
-yflag accepts default settings)Install Dependencies: Install Express for the web server, the Plivo SDK to interact with their API, and
dotenvto manage environment variables.Dependency Versions (2025):
express@^5.1.0: Latest stable Express 5.x (use^4.21.2for Express 4.x)plivo@^4.67.1: Current Plivo SDKdotenv@^16.4.5: Environment variable managementCreate Project Structure: Set up a basic structure for clarity.
mkdir routes # To hold your API route definitions touch server.js .env .gitignoreserver.js: The main entry point for your application.routes/: Directory to organize route handlers..env: File to store sensitive credentials (API keys, etc.). Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto prevent committing them. Open.gitignoreand add:Set up Basic Express Server (
server.js): Openserver.jsand add the initial Express setup.// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Use port from env var or default to 3000 // Middleware to parse JSON request bodies app.use(express.json()); // Middleware to parse URL-encoded request bodies app.use(express.urlencoded({ extended: true })); // Basic root route (optional) app.get('/', (req, res) => { res.send('Plivo SMS Sender API is running!'); }); // Placeholder for SMS routes (we'll add this soon) // const smsRoutes = require('./routes/sms'); // app.use('/api', smsRoutes); // Start the server app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });require('dotenv').config();: Loads variables from.env. Call this early.express.json()andexpress.urlencoded(): Middleware needed to parse incoming request bodies.process.env.PORT || 3000: Allows configuring the port via environment variables, essential for deployment platforms.2. How to Send SMS with Plivo API: Implementing Core Functionality
Create the logic to interact with the Plivo API using their Node.js SDK.
Create SMS Route File: Inside the
routesdirectory, create a file namedsms.js.touch routes/sms.jsImplement the Sending Logic (
routes/sms.js): This file defines the router and the handler for your/send-smsendpoint.// routes/sms.js const express = require('express'); const plivo = require('plivo'); const router = express.Router(); // Validate environment variables const AUTH_ID = process.env.PLIVO_AUTH_ID; const AUTH_TOKEN = process.env.PLIVO_AUTH_TOKEN; const SENDER_NUMBER = process.env.PLIVO_SENDER_NUMBER; // Your Plivo number or Sender ID if (!AUTH_ID || !AUTH_TOKEN || !SENDER_NUMBER) { console.error('Error: Plivo credentials or sender number not configured in .env file.'); // In a real app, you might prevent the server from starting or handle this more gracefully. // For this guide, we'll log the error and let requests fail if credentials aren't set. } // Initialize Plivo client (only if credentials exist) let client; if (AUTH_ID && AUTH_TOKEN) { client = new plivo.Client(AUTH_ID, AUTH_TOKEN); } // POST /api/send-sms router.post('/send-sms', async (req, res) => { // Basic input validation const { to, text } = req.body; if (!to || !text) { return res.status(400).json({ success: false, error: 'Missing required fields: "to" and "text".' }); } // Basic E.164 format check (simple version) // A more robust validation library (like libphonenumber-js) is recommended for production, // as it handles varying international lengths, country codes, and number types more accurately. if (!/^\+\d{1,15}$/.test(to)) { return res.status(400).json({ success: false, error: 'Invalid "to" number format. Use E.164 format (e.g., +14155552671).' }); } if (!client) { console.error('Plivo client not initialized due to missing credentials.'); return res.status(500).json({ success: false, error: 'Server configuration error: Plivo client not available.' }); } try { console.log(`Attempting to send SMS to ${to} from ${SENDER_NUMBER}`); const response = await client.messages.create({ src: SENDER_NUMBER, // Sender ID or Plivo Number from .env dst: to, // Destination number from request body text: text, // Message text from request body // Optional parameters: // type: 'sms', // Message type (sms or mms) // url: 'https://your-callback.com/dlr', // Delivery receipt callback URL // method: 'POST', // Callback method // log: true, // Enable logging }); console.log('Plivo API Response:', response); // Plivo API generally returns a 202 Accepted on success // The response contains message UUIDs res.status(202).json({ success: true, message: 'SMS send request accepted by Plivo.', plivoResponse: response, }); } catch (error) { console.error('Error sending SMS via Plivo:', error); // Provide a more specific error message if possible const errorMessage = error.message || 'Failed to send SMS.'; const statusCode = error.statusCode || 500; // Use Plivo's status code if available res.status(statusCode).json({ success: false, error: 'Plivo API Error', details: errorMessage, plivoError: error // Include the raw error for debugging if appropriate }); } }); module.exports = router; // Export the routerexpressandplivo.express.Router().process.env. Add checks to ensure they exist.POST /send-smshandler usesasync/awaitfor cleaner asynchronous code.toandtextare provided andtoroughly matches E.164 format. A note about using better libraries likelibphonenumber-jsis included.client.messages.create()sends the SMS. Key parameters:src(sender),dst(recipient),text(message content). Optional parameters includetype,url(delivery receipt callback),method, andlog.Mount the Router in
server.js: Uncomment and add the lines inserver.jsto use the router you just created.// server.js require('dotenv').config(); const express = require('express'); const smsRoutes = require('./routes/sms'); // Import the router const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get('/', (req, res) => { res.send('Plivo SMS Sender API is running!'); }); // Mount the SMS routes under the /api path app.use('/api', smsRoutes); // Use the imported router app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });Now, requests to
POST /api/send-smswill be handled by yourroutes/sms.jslogic.3. Building and Testing Your SMS API Endpoint
You've already implemented the core API endpoint (
POST /api/send-sms) in the previous step. This section details its usage and provides testing examples.API Endpoint Specification:
POST/api/send-smsapplication/jsonRequest Body Parameters:
to+14155552671textHello from Plivo!Response Status Codes:
Example
curlRequest:Replace placeholders with your actual server address, recipient number, and message.
curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "+14155552671", "text": "Hello from your Node.js Plivo App!" }'Example Success Response (HTTP 202 Accepted):
{ "success": true, "message": "SMS send request accepted by Plivo.", "plivoResponse": { "message": "message(s) queued", "messageUuid": [ "1f7a77ca-f1b7-11ee-9d7c-0242ac110003" ], "apiId": "1f7a6efc-f1b7-11ee-9d7c-0242ac110003" } }Example Error Response (HTTP 400 Bad Request – Missing Field):
{ "success": false, "error": "Missing required fields: \"to\" and \"text\"." }Example Error Response (HTTP 400 Bad Request – Invalid Number Format):
{ "success": false, "error": "Invalid \"to\" number format. Use E.164 format (e.g., +14155552671)." }Example Error Response (HTTP 500 Internal Server Error – Plivo API Failure):
{ "success": false, "error": "Plivo API Error", "details": "Resource not found", "plivoError": { /* Full Plivo error object */ } }4. Plivo Authentication: Configuring Your API Credentials
Proper configuration and secure handling of credentials are critical.
Obtain Plivo Credentials and Sender Number:
+12025551234).Configure Environment Variables (
.envfile): Open the.envfile in your project's root directory and add your credentials. Replace the placeholder values.PLIVO_AUTH_ID: Your unique authentication identifier.PLIVO_AUTH_TOKEN: Your secret authentication token. Treat this like a password.PLIVO_SENDER_NUMBER: The number or ID messages will appear to come from. Must be a valid Plivo number (for US/Canada) or a registered Sender ID where applicable.Load Environment Variables: The
require('dotenv').config();line at the top ofserver.jsloads these variables intoprocess.env, making them accessible throughout your application (as seen inroutes/sms.js).Security: The
.envfile keeps your sensitive credentials out of your source code. Ensure your.gitignorefile includes.envto prevent accidentally committing it. In production environments, set these variables directly in the deployment platform's configuration settings, not via a.envfile.Platform-Specific Configuration:
heroku config:set PLIVO_AUTH_ID=xxx)-eflag or docker-compose.ymlFallback Mechanisms: For this simple guide, there's no fallback. In a production system requiring high availability, consider:
5. Error Handling Best Practices for SMS Applications
Robust error handling and logging are crucial for debugging and reliability.
Error Handling Strategy:
client.messages.create()call. Log the detailed error. Return an appropriate HTTP status code (often 500 Internal Server Error, or useerror.statusCodeif Plivo's SDK provides it) and a JSON error message including details from the Plivo error.try...catchblocks to prevent unhandled promise rejections from crashing the server.Implement Global Error Handler: Add this middleware at the end of
server.js, after all routes:// Global error handler (add after all routes in server.js) app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(err.statusCode || 500).json({ success: false, error: err.message || 'Internal server error', ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); // Handle 404 errors app.use((req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found' }); });Logging:
console.logfor successful operations and Plivo responses, andconsole.errorfor errors. This is suitable for development.Example Winston Setup:
// logger.js const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple() })); } module.exports = logger;Usage:
const logger = require('./logger'); logger.info('Attempting to send SMS', { to, from: SENDER_NUMBER }); logger.error('Plivo API error', { error: error.message, statusCode: error.statusCode });Retry Mechanisms:
Sending an SMS is often idempotent from Plivo's side (sending the same request usually doesn't result in duplicate messages if the first was accepted), but network issues could prevent the request from reaching Plivo.
For critical messages, implement a simple retry strategy with exponential backoff for network errors or specific transient Plivo errors (e.g., 5xx status codes). Libraries like
async-retrycan help.Example (Conceptual):
// Conceptual retry logic using async-retry const retry = require('async-retry'); // ... inside the route handler ... try { await retry(async bail => { try { const response = await client.messages.create(/* ... */); console.log('Plivo API Response:', response); return response; } catch (error) { // Don't retry for permanent errors if (error.statusCode >= 400 && error.statusCode < 500) { bail(new Error(`Non-retryable Plivo error: ${error.message}`)); } // Retry for 5xx errors or network issues console.warn(`Retrying Plivo request due to error: ${error.message}`); throw error; // Trigger retry } }, { retries: 3, factor: 2, minTimeout: 1000, }); res.status(202).json(/* ... */); } catch (error) { console.error('Error sending SMS via Plivo after retries:', error); res.status(error.statusCode || 500).json(/* ... */); }Caution: Don't retry indefinitely or for errors that won't resolve (like invalid credentials or invalid recipient numbers).
Common Plivo Error Codes:
toortext.tonumber formats..envwith incorrectPLIVO_AUTH_IDorPLIVO_AUTH_TOKENto test authentication failures.PLIVO_SENDER_NUMBERto an invalid one.6. Database Schema and Data Layer
For this specific guide (only sending an SMS via an API call), a database is not strictly required.
However, in a real-world application, add a database (like PostgreSQL, MySQL, MongoDB) to:
messageUuid, recipient, sender, timestamp, status (initially "queued", potentially updated via webhooks), and message content. This aids auditing and tracking.Example Database Schema (PostgreSQL):
CREATE TABLE messages ( id SERIAL PRIMARY KEY, plivo_message_uuid VARCHAR(255) UNIQUE NOT NULL, recipient_number VARCHAR(20) NOT NULL, sender_id VARCHAR(20) NOT NULL, message_text TEXT NOT NULL, status VARCHAR(50) DEFAULT 'queued', error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, delivered_at TIMESTAMP ); CREATE INDEX idx_messages_status ON messages(status); CREATE INDEX idx_messages_recipient ON messages(recipient_number); CREATE INDEX idx_messages_created_at ON messages(created_at DESC);Field Descriptions:
idplivo_message_uuidrecipient_numbersender_idmessage_textstatuserror_messagecreated_atupdated_atdelivered_at(Full implementation details omitted as they are beyond the scope of this basic SMS sending guide).
7. Security Best Practices for SMS APIs
Enhance the security of your API endpoint.
Input Validation and Sanitization:
toandtext. For production, use a robust validation library likejoiorexpress-validator.Example with Joi:
const Joi = require('joi'); const smsSchema = Joi.object({ to: Joi.string() .pattern(/^\+\d{1,15}$/) .required() .messages({ 'string.pattern.base': 'Phone number must be in E.164 format (e.g., +14155552671)', 'any.required': 'Recipient phone number is required' }), text: Joi.string() .min(1) .max(1600) .required() .messages({ 'string.max': 'Message text must not exceed 1600 characters', 'any.required': 'Message text is required' }) }); // In route handler: const { error, value } = smsSchema.validate(req.body); if (error) { return res.status(400).json({ success: false, error: error.details[0].message }); }Common Vulnerabilities & Protections:
X-API-Key) or using more robust methods like JWT.X-Content-Type-Options,Referrer-Policy,Strict-Transport-Security,X-Frame-Optionsto mitigate common web vulnerabilities.Implement Rate Limiting: Use a library like
express-rate-limit.Install:
npm install express-rate-limitApply in
server.js:// server.js require('dotenv').config(); const express = require('express'); const rateLimit = require('express-rate-limit'); // Import const smsRoutes = require('./routes/sms'); const app = express(); const PORT = process.env.PORT || 3000; // Apply rate limiting to API routes const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs 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 }); app.use('/api', apiLimiter); // Apply the limiter to /api routes app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Basic root route app.get('/', (req, res) => { res.send('Plivo SMS Sender API is running!'); }); app.use('/api', smsRoutes); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });Adjust
windowMsandmaxaccording to your expected usage and security requirements.Implement Basic Security Headers: Use the
helmetmiddleware.Install:
npm install helmetApply in
server.js:// server.js require('dotenv').config(); const express = require('express'); const rateLimit = require('express-rate-limit'); const helmet = require('helmet'); // Import const smsRoutes = require('./routes/sms'); const app = express(); const PORT = process.env.PORT || 3000; app.use(helmet()); // Use Helmet for basic security headers const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs 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 }); app.use('/api', apiLimiter); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Basic root route app.get('/', (req, res) => { res.send('Plivo SMS Sender API is running!'); }); app.use('/api', smsRoutes); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });helmet()applies several default security-related HTTP headers.Protecting API Keys: Already covered by using
.envand.gitignore. Ensure production environments use secure configuration management.Testing for Vulnerabilities:
npm audit).8. Understanding SMS Message Format and Compliance
E.164 Format: Strictly enforce the
+prefix and country code format fortonumbers. Plivo requires this. Considerlibphonenumber-jsfor robust parsing and validation, as it understands nuances like variable number lengths per country.Character Encoding (GSM vs. Unicode):
Plivo handles encoding automatically based on the
textcontent. Longer messages or messages with Unicode characters split into multiple segments (concatenated on the recipient's device) and billed accordingly. See Plivo's Encoding and Concatenation guide.Message Length Examples:
Sender ID Rules: US/Canada requires a Plivo number. Other countries have varying rules for numeric vs. alphanumeric sender IDs, often requiring pre-registration. Using an invalid or unregistered sender ID will cause message delivery failure. Check Plivo's country-specific SMS guidelines.
Trial Account Limitations: Sending is restricted to verified "Sandbox" numbers.
Opt-Out Handling: Regulations (like TCPA in the US) require handling STOP and HELP keywords. Plivo can manage standard opt-outs automatically for long codes and Toll-Free numbers if you configure this in the console. Ensure compliance with local regulations.
Compliance Requirements by Region:
9. Performance Optimization for High-Volume SMS
For this simple single-message API, major optimizations are usually unnecessary. However, for higher volume:
async/awaitmaintains this benefit with cleaner syntax.Example BullMQ Queue Setup:
// Install: npm install bullmq ioredis // queue.js const { Queue } = require('bullmq'); const smsQueue = new Queue('sms-queue', { connection: { host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, } }); module.exports = smsQueue; // In route handler (routes/sms.js): const smsQueue = require('../queue'); router.post('/send-sms', async (req, res) => { const { to, text } = req.body; // Add to queue instead of sending immediately await smsQueue.add('send-sms', { to, text, from: SENDER_NUMBER }); res.status(202).json({ success: true, message: 'SMS queued for sending' }); }); // worker.js (separate process) const { Worker } = require('bullmq'); const plivo = require('plivo'); const client = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN); const worker = new Worker('sms-queue', async job => { const { to, text, from } = job.data; await client.messages.create({ src: from, dst: to, text }); }, { connection: { host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, } });k6,Artillery, orApacheBench(ab) to simulate concurrent users and identify bottlenecks under load. Monitor CPU, memory, and response times during tests.Example k6 Load Test:
// load-test.js import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '30s', target: 10 }, // Ramp up to 10 users { duration: '1m', target: 10 }, // Stay at 10 users { duration: '30s', target: 0 }, // Ramp down ], }; export default function () { const payload = JSON.stringify({ to: '+14155552671', text: 'Load test message', }); const params = { headers: { 'Content-Type': 'application/json', }, }; const res = http.post('http://localhost:3000/api/send-sms', payload, params); check(res, { 'status is 202': (r) => r.status === 202, 'response time < 500ms': (r) => r.timings.duration < 500, }); sleep(1); }node --prof) or tools like Clinic.js to identify performance hotspots in your code.10. Monitoring and Observability for SMS Applications
For production readiness:
Health Checks: Add a simple health check endpoint that verifies basic application status.
// Add to server.js app.get('/health', (req, res) => { // Basic check: just confirm the server is running // More advanced: check DB connection, Plivo connectivity res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); });Performance Metrics: Monitor key metrics like request latency, request rate (RPM), error rates (4xx, 5xx), and resource usage (CPU, memory). Tools like Prometheus/Grafana, Datadog APM, or New Relic can track these.
Example Prometheus Metrics:
// Install: npm install prom-client // metrics.js const client = require('prom-client'); const register = new client.Registry(); client.collectDefaultMetrics({ register }); const httpRequestDuration = new client.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['method', 'route', 'status_code'], registers: [register] }); const smsCounter = new client.Counter({ name: 'sms_sent_total', help: 'Total number of SMS messages sent', labelNames: ['status'], registers: [register] }); module.exports = { register, httpRequestDuration, smsCounter }; // In server.js: const { register } = require('./metrics'); app.get('/metrics', async (req, res) => { res.setHeader('Content-Type', register.contentType); res.send(await register.metrics()); });Complete Sentry Integration:
// Install: npm install @sentry/node @sentry/tracing // At the top of server.js, before other imports const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV || 'development', tracesSampleRate: 1.0, integrations: [ new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app }), ], }); // Request handler must be first middleware app.use(Sentry.Handlers.requestHandler()); app.use(Sentry.Handlers.tracingHandler()); // All your routes go here // Error handler must be before any other error middleware app.use(Sentry.Handlers.errorHandler());console.logwith a structured logging library (Winston, Pino) that supports log levels, JSON formatting, and multiple transports (console, files, external services).Deploying Your SMS Application to Production
Environment Configuration: Use environment-specific
.envfiles or platform configuration (Heroku Config Vars, AWS Parameter Store, etc.) to manage credentials and settings across development, staging, and production.Process Management: Use a process manager like PM2 or systemd to keep your Node.js application running, handle crashes with automatic restarts, and manage multiple instances for load balancing.
Example PM2 Configuration:
// ecosystem.config.js module.exports = { apps: [{ name: 'plivo-sms-api', script: './server.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' }] }; // Start: pm2 start ecosystem.config.js // Monitor: pm2 monit // Logs: pm2 logsLoad Balancing: Deploy multiple instances of your application behind a load balancer (NGINX, AWS ALB, etc.) to distribute traffic and ensure high availability.
SSL/TLS: Always use HTTPS in production. Obtain SSL certificates from Let's Encrypt or your cloud provider.
Security Hardening: Review OWASP Top 10 vulnerabilities, implement proper input validation, use security headers (helmet), enable rate limiting, and keep dependencies updated.
Monitoring and Alerting: Set up alerts for critical metrics like high error rates, slow response times, or service downtime. Use tools like PagerDuty, Opsgenie, or cloud provider alerting.
Backup and Disaster Recovery: If you add a database, implement regular backups and test your disaster recovery procedures.
Cost Optimization: Monitor your Plivo usage and costs. Implement message queuing for batch sends to optimize throughput. Consider caching frequently accessed data to reduce API calls.
Platform-Specific Deployment:
Heroku:
# Install Heroku CLI, then: heroku create plivo-sms-api heroku config:set PLIVO_AUTH_ID=xxx PLIVO_AUTH_TOKEN=xxx PLIVO_SENDER_NUMBER=+1xxx git push heroku mainAWS Elastic Beanstalk:
# Install EB CLI, then: eb init plivo-sms-api --platform node.js --region us-east-1 eb create production eb setenv PLIVO_AUTH_ID=xxx PLIVO_AUTH_TOKEN=xxx PLIVO_SENDER_NUMBER=+1xxx eb deployDocker:
Related Resources
This comprehensive guide provides everything you need to send SMS messages with Plivo, Node.js, and Express. Build upon these patterns to create more sophisticated messaging features tailored to your application's needs.