code examples
code examples
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.
Send SMS Messages with Node.js, Express, and Plivo
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. | ^16.0.0 or later |
System Architecture:
+-------------+ +-----------------------+ +-----------------+ +-----------------+
| | HTTP | | Plivo | | SMS | |
| User/Client | ----> | Node.js/Express App | ----> | Plivo API | ----> | Recipient Phone |
| (e.g. curl) | POST | (API Endpoint /send-sms)| SDK | (Sends Message) | | |
+-------------+ +-----------------------+ +-----------------+ +-----------------+
| Loads Credentials
| from .env
v
+---------+
| .env |
| (Secrets)|
+---------+Prerequisites:
- Node.js and npm (or yarn): Download from nodejs.org. Recommended: Node.js v22 (LTS) or v20 (LTS) for production use in 2025.
- Plivo Account: Sign up at plivo.com.
- 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)
curlor 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)
- Multi-segment SMS (320 characters): 2 × $0.0085 = $0.017 per message
- 1,000 messages/day: ~$8.50/day or $255/month (single segment, AT&T/T-Mobile)
- Unicode/emoji messages (70 characters): Higher segment count increases costs proportionally
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.
bashmkdir plivo-node-sms-guide cd plivo-node-sms-guide -
Initialize Node.js Project: Create a
package.jsonfile to manage project dependencies and scripts.bashnpm init -y(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.bashnpm install express@^5.1.0 plivo@^4.67.1 dotenv@^16.4.5Dependency 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 management
-
Create Project Structure: Set up a basic structure for clarity.
bashmkdir 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:text# .gitignore # Dependencies node_modules # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* -
Set up Basic Express Server (
server.js): Openserver.jsand add the initial Express setup.javascript// 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.bashtouch routes/sms.js -
Implement the Sending Logic (
routes/sms.js): This file defines the router and the handler for your/send-smsendpoint.javascript// 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 router- Import
expressandplivo. - 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-smshandler usesasync/awaitfor cleaner asynchronous code. - Input Validation: Basic checks ensure
toandtextare provided andtoroughly matches E.164 format. A note about using better libraries likelibphonenumber-jsis included. - Plivo Call:
client.messages.create()sends the SMS. Key parameters:src(sender),dst(recipient),text(message content). Optional parameters includetype,url(delivery receipt callback),method, andlog. - 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.
- Import
-
Mount the Router in
server.js: Uncomment and add the lines inserver.jsto use the router you just created.javascript// 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:
| 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.
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:
- Log in to your Plivo Console.
- 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).
- 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.,
-
Configure Environment Variables (
.envfile): Open the.envfile in your project's root directory and add your credentials. Replace the placeholder values.dotenv# .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_IDPLIVO_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 .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
-eflag 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 useerror.statusCodeif 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...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: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.logfor successful operations and Plivo responses, andconsole.errorfor 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).
Example Winston Setup:
javascript// 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:
javascriptconst logger = require('./logger'); logger.info('Attempting to send SMS', { to, from: SENDER_NUMBER }); logger.error('Plivo API error', { error: error.message, statusCode: error.statusCode }); - Current: We use
-
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):
javascript// 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:
| 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
toortext. - Send requests with invalid
tonumber formats. - Temporarily modify
.envwith incorrectPLIVO_AUTH_IDorPLIVO_AUTH_TOKENto test authentication failures. - Temporarily modify
PLIVO_SENDER_NUMBERto 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.
- Send requests missing
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):
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:
| 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
toandtext. For production, use a robust validation library likejoiorexpress-validator.
Example with Joi:
javascriptconst 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 }); } - Validation: We already implemented basic validation for
-
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-Optionsto mitigate common web vulnerabilities.
- 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 (
-
Implement Rate Limiting: Use a library like
express-rate-limit.-
Install:
npm install express-rate-limit -
Apply in
server.js:javascript// 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 helmet -
Apply in
server.js:javascript// 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:
- 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.
- Check dependency vulnerabilities regularly (
npm audit).
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.
Character Encoding (GSM vs. Unicode):
| Encoding | Characters per Segment | Triggers |
|---|---|---|
| GSM 03.38 | 160 | Standard Latin characters, basic punctuation |
| Unicode (UCS-2) | 70 | Emojis, non-Latin characters (Arabic, Chinese, etc.) |
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/awaitmaintains this benefit with cleaner syntax. - Connection Pooling: The Plivo SDK likely handles underlying HTTP connection management. No specific action needed here.
- Payload Size: Keep request/response payloads minimal. Avoid sending excessively large JSON responses.
- 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.
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,
}
});- Caching: Not applicable for sending unique messages. Could be relevant if fetching contact info or templates frequently.
- Load Testing: Use tools like
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);
}- 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: 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());- Logging Best Practices: For production, replace
console.logwith 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.
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:
# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# Build: docker build -t plivo-sms-api .
# Run: docker run -p 3000:3000 --env-file .env plivo-sms-apiRelated Resources
- E.164 Phone Number Format Guide
- Understanding 10DLC Registration for SMS
- Plivo SMS Pricing and Rates
- Two-Factor Authentication (2FA) Implementation Guide
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
How to send SMS messages with Node.js and Express?
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.
What is Plivo used for in this Node.js application?
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.
Why does the Node.js app need Express.js?
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.
When should I use a Plivo number versus a Sender ID for SMS?
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.
Can I send SMS to any number with a Plivo trial account?
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.
How to set up a Node.js project for sending SMS with Plivo?
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.
What is the role of the .env file when using Plivo?
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.
How do I handle errors when sending SMS with the Plivo API?
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.
What format should the recipient phone number (\"to\") be in?
Use the E.164 international number format, which includes a '+' prefix followed by the country code and phone number (e.g., +12025550100).
How to secure a Node.js Express API for sending SMS via Plivo?
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'.
How to send SMS messages to multiple recipients using Node and Plivo?
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.
What should I do if the Plivo API request fails?
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.
What data should be logged when using Plivo?
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.
How to test the Plivo SMS API integration?
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.