code examples
code examples
Implement Plivo SMS/MMS Delivery Status Callbacks in Node.js
A step-by-step guide to building a Node.js (Express) service to receive, validate, process, and store Plivo SMS/MMS delivery status updates using webhooks, with optional WebSocket integration.
Reliably tracking the delivery status of SMS and MMS messages is crucial for applications that depend on timely communication. Misdelivered or failed messages can lead to poor user experience, missed notifications, and operational issues. Plivo provides a robust mechanism using webhooks (callbacks) to notify your application in real-time about the status of sent messages.
This guide provides a step-by-step walkthrough for building a production-ready Node.js (Express) backend service to receive, validate, process, and store Plivo SMS/MMS delivery status updates. We'll also cover sending messages and optionally pushing these status updates to a frontend (like React/Vue built with Vite) via WebSockets.
Project Goals:
- Receive Plivo Callbacks: Set up a secure endpoint to receive HTTP POST requests from Plivo containing message status updates.
- Validate Requests: Ensure incoming requests genuinely originate from Plivo using signature validation.
- Process Status Updates: Parse the callback data to understand the message status (e.g.,
queued,sent,delivered,failed). - Persist Status Data: Store message status information in a database for tracking, analysis, and auditing.
- Real-time Notifications (Optional): Implement a WebSocket server to push status updates to connected clients (e.g., a frontend dashboard).
- Send Test Messages: Create an endpoint to initiate sending an SMS/MMS message via Plivo, triggering the callback workflow.
Technologies Used:
- Node.js: JavaScript runtime environment for the backend.
- Express.js: Web framework for Node.js to create the API endpoint.
- Plivo Node SDK: Official library for interacting with the Plivo API (sending messages and validating callbacks).
- PostgreSQL: Relational database for storing status updates.
- Prisma: Next-generation ORM for Node.js and TypeScript for database interaction.
ws: WebSocket library for real-time communication.dotenv: Module to load environment variables from a.envfile.- Ngrok (Development): Tool to expose your local server to the internet for Plivo callbacks.
Prerequisites:
- Node.js and npm (or yarn) installed.
- A Plivo account with Auth ID and Auth Token. (Sign up at Plivo)
- A Plivo phone number capable of sending SMS/MMS.
- Access to a PostgreSQL database (local or cloud-hosted).
- A text editor or IDE (like VS Code).
- Basic understanding of Node.js, Express, APIs, and databases.
- Ngrok installed (optional, for local development testing).
System Architecture:
+-----------------+ 1. Send SMS/MMS +-----------------+ 2. Status Update +---------------------+
| Your Application| ----------------------> | Plivo API | ----------------------> | Node.js Callback EP |
| (e.g., Frontend)| +-----------------+ | (This Guide) |
+-----------------+ +----------+----------+
^ | 3. Validate Req.
| 6. WebSocket Update (Optional) |
| | 4. Process Status
+----+-----------+ 5. Store Status +-----------------+ |
| WebSocket | <--------------------- | PostgreSQL DB | <----------------------------------+
| Clients | +-----------------+
+----------------+Final Outcome:
By the end of this guide, you will have a robust Node.js service capable of:
- Receiving and securely validating Plivo message delivery callbacks.
- Storing detailed status updates in a database.
- Sending SMS/MMS messages via Plivo that trigger these callbacks.
- Optionally, broadcasting status updates in real-time via WebSockets.
1. Project Setup
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project.
bashmkdir plivo-callback-guide cd plivo-callback-guide -
Initialize Node.js Project:
bashnpm init -yThis creates a
package.jsonfile. -
Install Dependencies: We need Express for the server, the Plivo SDK,
dotenvfor environment variables,wsfor WebSockets, and Prisma for database interaction.bashnpm install express plivo dotenv ws npm install prisma @prisma/client --save-devexpress: Web server framework.plivo: Plivo's official Node.js SDK.dotenv: Loads environment variables from a.envfile.ws: WebSocket server implementation.prisma: Prisma CLI (dev dependency).@prisma/client: Prisma database client.
-
Initialize Prisma: Set up Prisma with PostgreSQL as the provider.
bashnpx prisma init --datasource-provider postgresqlThis creates a
prismadirectory with aschema.prismafile and a.envfile (if one doesn't exist). -
Configure Environment Variables: Open the
.envfile created by Prisma (or create one) and add your Plivo credentials and database connection string. Replace the placeholder values with your actual credentials and URLs.dotenv# .env # Plivo Credentials # Get these from your Plivo Console: https://console.plivo.com/dashboard/ PLIVO_AUTH_ID="YOUR_PLIVO_AUTH_ID" PLIVO_AUTH_TOKEN="YOUR_PLIVO_AUTH_TOKEN" PLIVO_PHONE_NUMBER="YOUR_PLIVO_PHONE_NUMBER" # Sender number (e.g., +14155551212) # Database Connection (Prisma) # Example: postgresql://user:password@host:port/database?schema=public DATABASE_URL="postgresql://user:password@localhost:5432/plivo_callbacks?schema=public" # Application Settings PORT=3000 # Replace with your Ngrok URL (dev) or public server URL (prod) # Example: https://yourapp.com OR https://your-subdomain.ngrok.io BASE_CALLBACK_URL="YOUR_PUBLICLY_ACCESSIBLE_URL"BASE_CALLBACK_URLis crucial. Plivo needs a public URL to send callbacks to. We'll set this later using Ngrok for development.
-
Create
.gitignore: Create a.gitignorefile in the root directory to avoid committing sensitive information and unnecessary files.text# .gitignore node_modules .env dist npm-debug.log* yarn-debug.log* yarn-error.log* prisma/dev.db* -
Define Database Schema: Open
prisma/schema.prismaand define a model to store message status updates.prisma// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model MessageStatus { id String @id @default(cuid()) // Unique identifier for the status record messageUUID String @unique // Plivo's unique identifier for the message status String // e.g., queued, sent, delivered, failed, undelivered errorCode String? // Plivo error code if status is failed/undelivered rawPayload Json // Store the full callback payload for reference receivedAt DateTime @default(now()) // Timestamp when the callback was received updatedAt DateTime @updatedAt // Timestamp when the record was last updated @@index([messageUUID]) @@index([status]) @@index([receivedAt]) }- This schema defines a
MessageStatustable with relevant fields. messageUUIDis marked as unique to potentially update existing records (upsert) if multiple callbacks arrive for the same message.rawPayloadstores the complete JSON received from Plivo, useful for debugging or future analysis.
- This schema defines a
-
Apply Database Schema: Run the Prisma migration command to create the
MessageStatustable in your database. Prisma will prompt you to create a migration file.bashnpx prisma migrate dev --name init-message-statusAlternatively, if you prefer not to use migration files during early development (use with caution):
bash# npx prisma db pushEnsure your PostgreSQL database server is running and accessible using the
DATABASE_URLin your.envfile. -
Create Server File: Create a file named
server.jsin the root directory. This will contain our Express application logic.javascript// server.js require('dotenv').config(); // Load .env variables first const express = require('express'); const plivo = require('plivo'); const { PrismaClient } = require('@prisma/client'); const WebSocket = require('ws'); // --- Basic Configuration --- const app = express(); const port = process.env.PORT || 3000; const prisma = new PrismaClient(); // Plivo Client Initialization if (!process.env.PLIVO_AUTH_ID || !process.env.PLIVO_AUTH_TOKEN) { console.error("Error: Plivo Auth ID or Auth Token missing in .env file."); process.exit(1); } const plivoClient = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN); // --- Middleware --- // Use Express's built-in JSON parser for most routes app.use(express.json()); // Use raw body parser specifically for Plivo validation middleware // It MUST come before the validation middleware and handle potential content types app.use('/plivo/callback', express.raw({ type: ['application/json', 'application/x-www-form-urlencoded'] })); // --- WebSocket Setup (Optional) --- const wss = new WebSocket.Server({ noServer: true }); // Attach to HTTP server later const clients = new Set(); // Store connected WebSocket clients wss.on('connection', (ws) => { console.log('WebSocket client connected'); clients.add(ws); ws.on('message', (message) => { // Handle messages from clients if needed console.log('Received WebSocket message:', message.toString()); }); ws.on('close', () => { console.log('WebSocket client disconnected'); clients.delete(ws); }); ws.on('error', (error) => { console.error('WebSocket error:', error); clients.delete(ws); // Remove on error }); ws.send(JSON.stringify({ message: 'Connected to Plivo Status WebSocket' })); }); // Function to broadcast messages to all connected WebSocket clients function broadcast(data) { const message = JSON.stringify(data); clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(message, (err) => { if (err) { console.error('WebSocket send error:', err); // Optionally remove client if send fails repeatedly // clients.delete(client); } }); } }); } // --- Routes --- app.get('/', (req, res) => { res.send('Plivo Callback Server is Running!'); }); // Endpoint to receive Plivo callbacks (Implementation in next steps) app.post('/plivo/callback', /* ... Plivo Validation Middleware ... */ async (req, res) => { // Callback processing logic here (will be added in Step 3) console.log('Placeholder for /plivo/callback POST'); res.status(200).send('Callback received (placeholder)'); }); // Endpoint to send a test SMS (Implementation in later steps) app.post('/send-test-sms', async (req, res) => { // SMS sending logic here (will be added in Step 7) console.log('Placeholder for /send-test-sms POST'); res.status(501).send('Not Implemented Yet'); }); // --- Global Error Handler --- app.use((err, req, res, next) => { console.error("Unhandled Error:", err.stack || err); res.status(500).json({ error: 'Something went wrong!' }); }); // --- Start Server --- const server = app.listen(port, () => { console.log(`Server listening on http://localhost:${port}`); }); // Attach WebSocket server to the HTTP server server.on('upgrade', (request, socket, head) => { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); }); // Graceful shutdown handler const shutdown = async (signal) => { console.log(`${signal} signal received: closing HTTP server`); server.close(async () => { console.log('HTTP server closed'); await prisma.$disconnect(); console.log('Prisma client disconnected'); wss.close(() => { console.log('WebSocket server closed'); process.exit(0); // Exit after cleanup }); }); }; // Listen for termination signals process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));This sets up the basic Express server, initializes Prisma and the WebSocket server, and defines placeholders for our routes and error handling. Note the use of
express.rawspecifically for the callback route – Plivo's signature validation needs the raw, unparsed request body.
2. Plivo Setup and Callback URL
Before Plivo can send status updates, you need to tell it where to send them. This requires a publicly accessible URL.
- Get Plivo Credentials: If you haven't already, log in to your Plivo Console and note your
Auth IDandAuth Token. Ensure they are correctly set in your.envfile. - Expose Local Server (Development): For development, use Ngrok to create a secure tunnel to your local machine.
- Open a new terminal window (leave your server running in the first).
- Run Ngrok, pointing it to the port your server is listening on (default 3000).
bash
ngrok http 3000 - Ngrok will display forwarding URLs (e.g.,
https://random-string.ngrok.io). Copy thehttpsURL. This is your public base URL.
- Update
.env: Paste the Ngrok HTTPS URL (or your production URL) into your.envfile as theBASE_CALLBACK_URL.Important: Restart your Node.js server (dotenv# .env # ... other variables BASE_CALLBACK_URL=""https://<your-ngrok-subdomain>.ngrok.io"" # Or https://your-production-app.comnode server.jsor usingnodemon) after updating the.envfile for the changes to take effect. Check the server startup logs to ensureBASE_CALLBACK_URLis loaded. - Configure Plivo Application (Optional but Recommended): While you can specify the callback URL per message, it's good practice to set a default at the application level in Plivo.
- Go to Plivo Console -> Messaging -> Applications.
- Click ""Add New Application"".
- Give it a name (e.g., ""Node Callback App"").
- In the ""Message URL"" field, enter your full callback URL:
${BASE_CALLBACK_URL}/plivo/callback. Example:https://<your-ngrok-subdomain>.ngrok.io/plivo/callback. - Leave other fields blank or as default for now.
- Click ""Create Application"". Plivo will assign an
App ID. - You can optionally link your Plivo number to this application under Phone Numbers -> Your Number -> Application Type: XML Application, Application: Choose your app. This makes the callback URL the default for messages sent from that number unless overridden in the API call.
3. Building the Callback Endpoint & Validation
Now, let's implement the /plivo/callback endpoint to receive and validate requests. Security is paramount here; we must ensure requests actually come from Plivo.
-
Implement Validation Middleware: Plivo includes
X-Plivo-Signature-V3,X-Plivo-Signature-V3-Nonce, andX-Plivo-Signature-V3-Timestampheaders. The SDK provides a utility to validate these. Modify theserver.jsfile:javascript// server.js // ... (imports and setup) ... // --- Middleware --- app.use(express.json()); // For other routes // Raw body parser MUST come before the validation middleware for the specific route app.use('/plivo/callback', express.raw({ type: ['application/json', 'application/x-www-form-urlencoded'] })); // Handle potential content types // Plivo Validation Middleware Function const validatePlivoSignature = (req, res, next) => { const signature = req.headers['x-plivo-signature-v3']; const nonce = req.headers['x-plivo-signature-v3-nonce']; const timestamp = req.headers['x-plivo-signature-v3-timestamp']; const method = req.method; // Typically POST // Construct the full URL Plivo used to send the request const url = process.env.BASE_CALLBACK_URL + req.originalUrl; // Use the raw body buffer for validation const rawBody = req.body; // Thanks to express.raw() middleware console.log('Received Plivo Callback:'); console.log(` URL: ${url}`); console.log(` Method: ${method}`); console.log(` Signature: ${signature ? 'Present' : 'MISSING!'}`); console.log(` Nonce: ${nonce ? 'Present' : 'MISSING!'}`); console.log(` Timestamp: ${timestamp ? 'Present' : 'MISSING!'}`); // console.log(` Raw Body: ${rawBody.toString()}`); // Uncomment for debugging (can be verbose/sensitive) if (!signature || !nonce || !timestamp || !process.env.PLIVO_AUTH_TOKEN) { console.warn('Missing Plivo signature headers or Auth Token'); return res.status(400).send('Missing Plivo signature headers or server misconfiguration'); } if (!Buffer.isBuffer(rawBody)) { console.error('Raw body is not a buffer. Check middleware order.'); return res.status(500).send('Internal Server Error: Body parsing issue'); } try { // Plivo SDK v6+ uses validateV3Signature const isValid = plivo.validateV3Signature( method, // HTTP Method (e.g., 'POST') url, // Full URL Plivo sent the request to nonce, timestamp, process.env.PLIVO_AUTH_TOKEN, // Use Auth Token for v3 validation signature, rawBody.toString() // Pass the raw body as a string ); if (isValid) { console.log('Plivo signature validation successful.'); // IMPORTANT: Parse the raw body AFTER validation if it's JSON // Store the parsed body back on req.body for the main handler if (req.is('application/json') && Buffer.isBuffer(rawBody)) { try { // Replace the buffer with the parsed object req.body = JSON.parse(rawBody.toString()); } catch (parseError) { console.error('Failed to parse JSON body after validation:', parseError); // Still proceed, but the handler will need to deal with the raw buffer // Or return an error if JSON is strictly required // return res.status(400).send('Invalid JSON payload'); } } else if (req.is('application/x-www-form-urlencoded') && Buffer.isBuffer(rawBody)) { // Plivo status callbacks are typically JSON, but handle just in case console.warn('Received form-urlencoded Plivo callback. Body kept as buffer.'); // If needed, parse using: const querystring = require('querystring'); // req.body = querystring.parse(rawBody.toString()); } // If not JSON or form-urlencoded, req.body remains the raw buffer next(); // Signature is valid, proceed to the route handler } else { console.warn('Invalid Plivo signature.'); return res.status(403).send('Invalid Plivo signature'); } } catch (error) { console.error('Error validating Plivo signature:', error); return res.status(500).send('Error validating signature'); } }; // --- Routes --- app.get('/', (req, res) => { res.send('Plivo Callback Server is Running!'); }); // Apply the validation middleware ONLY to the callback route app.post('/plivo/callback', validatePlivoSignature, async (req, res) => { // Process the validated callback (next step) let payload; let rawPayloadStr = ''; // Check if body was parsed in middleware, otherwise try to parse if it's a buffer if (Buffer.isBuffer(req.body)) { rawPayloadStr = req.body.toString(); // Attempt to parse if it wasn't handled in middleware (e.g., unexpected content type) try { payload = JSON.parse(rawPayloadStr); } catch (e) { console.error("Failed to parse request body buffer in handler:", e); // Send 200 OK anyway to prevent Plivo retries, but log the failure. // We won't be able to process this specific callback further. return res.status(200).send('Callback received but could not parse body'); } } else if (typeof req.body === 'object' && req.body !== null) { payload = req.body; // Already parsed (likely JSON by middleware) try { rawPayloadStr = JSON.stringify(payload); // For logging/storage if needed } catch (e) { console.error("Failed to stringify already parsed payload:", e); } } else { console.error("Unexpected body type in handler:", typeof req.body); return res.status(200).send('Callback received with unexpected body type'); } console.log("Validated Plivo Payload:", JSON.stringify(payload, null, 2)); // Acknowledge receipt immediately to Plivo res.status(200).send('Callback received'); // --- Asynchronous processing happens after sending the response --- try { const messageUUID = payload.MessageUUID; const status = payload.Status; const errorCode = payload.ErrorCode; // Present on failure/undelivered if (!messageUUID) { console.warn("Callback received without MessageUUID:", payload); return; // Can't process without UUID } console.log(`Processing status: ${status} for MessageUUID: ${messageUUID}`); // Store in Database (Step 5) const savedStatus = await prisma.messageStatus.upsert({ where: { messageUUID: messageUUID }, update: { status: status, errorCode: errorCode || null, // Ensure null if not present rawPayload: payload, // Store the parsed payload object as JSONB }, create: { messageUUID: messageUUID, status: status, errorCode: errorCode || null, rawPayload: payload, // Store the parsed payload object as JSONB }, }); console.log(`Saved/Updated status for ${messageUUID} in DB (ID: ${savedStatus.id})`); // Broadcast via WebSocket (Step 6) broadcast({ type: 'status_update', messageUUID: messageUUID, status: status, errorCode: errorCode || null, timestamp: savedStatus.updatedAt // Use DB timestamp }); console.log(`Broadcasted status update for ${messageUUID} via WebSocket`); } catch (error) { // Use optional chaining in case payload was unparseable or missing fields console.error(`Error processing callback for ${payload?.MessageUUID || 'unknown UUID'}:`, error); // Error is logged, but we already sent 200 OK to Plivo. // Implement internal alerting/monitoring for processing failures. } }); // ... (send-test-sms route, error handler, server start) ...- We create a
validatePlivoSignaturemiddleware function. - It extracts necessary headers and constructs the full URL Plivo called.
- Crucially, it uses
req.body(which contains the raw Buffer thanks toexpress.raw) and theplivo.validateV3Signaturemethod. Note: Plivo's V3 signature uses the Auth Token. - If validation passes, it calls
next(). Otherwise, it sends a403 Forbiddenor400 Bad Request. - Important: After successful validation, we attempt to parse the
rawBodyback intoreq.bodyas JSON if the content type was JSON. This makesreq.bodyaccessible as an object in the main route handler. Added robust handling in the main route to manage cases where parsing might fail or the body isn't a buffer. - The middleware is applied only to the
/plivo/callbackPOST route.
- We create a
4. Processing Callbacks
The previous step already included the processing logic inside the /plivo/callback route after validation and the immediate res.status(200).send():
// Inside app.post('/plivo/callback', validatePlivoSignature, async (req, res) => { ... })
// ... (Payload parsing logic) ...
// Acknowledge receipt immediately
res.status(200).send('Callback received');
// Asynchronous processing after response
try {
const messageUUID = payload.MessageUUID;
const status = payload.Status;
const errorCode = payload.ErrorCode; // e.g., ""401"", ""600""
if (!messageUUID) { /* ... handle missing UUID ... */ }
console.log(`Processing status: ${status} for MessageUUID: ${messageUUID}`);
// Store in Database (Step 5 - Implemented in Step 3)
// Broadcast via WebSocket (Step 6 - Implemented in Step 3)
} catch (error) {
// ... error handling ...
}- Acknowledge Quickly: The
res.status(200).send(...)happens before database or WebSocket operations. This confirms receipt to Plivo promptly, preventing timeouts and retries on their end. - Extract Data: Key fields like
MessageUUID,Status, andErrorCodeare extracted from the validatedpayloadobject. - Asynchronous Operations: Database saving and WebSocket broadcasting happen asynchronously after the response has been sent.
Common Statuses:
queued: Plivo has accepted the message.sent: Plivo has sent the message to the downstream carrier.delivered: The carrier confirmed delivery to the recipient's handset.failed: Plivo couldn't send the message (e.g., invalid number, API error).ErrorCodeprovides details.undelivered: The carrier couldn't deliver the message (e.g., number blocked, phone off, out of coverage).ErrorCodeprovides details.
Refer to Plivo Message States for a full list and ErrorCode details.
5. Storing Status Updates (Database)
We use Prisma to save the status updates to our PostgreSQL database. This logic was implemented within the try...catch block of the /plivo/callback route in Step 3:
// Inside the try block of app.post('/plivo/callback', ...)
// Store in Database
const savedStatus = await prisma.messageStatus.upsert({
where: { messageUUID: messageUUID }, // Find existing record by unique UUID
update: { // If found, update these fields
status: status,
errorCode: errorCode || null,
rawPayload: payload, // Update the full payload too (as JSON)
// updatedAt is handled automatically by Prisma @updatedAt
},
create: { // If not found, create a new record
messageUUID: messageUUID,
status: status,
errorCode: errorCode || null,
rawPayload: payload, // Store the full payload (as JSON)
// receivedAt handled by Prisma @default(now())
// updatedAt handled by Prisma @updatedAt
},
});
console.log(`Saved/Updated status for ${messageUUID} in DB (ID: ${savedStatus.id})`);prisma.messageStatus.upsertis used:- It tries to find a record where
messageUUIDmatches the incoming one. - If found (
update), it updates thestatus,errorCode,rawPayload, andupdatedAttimestamp. - If not found (
create), it creates a new record with all the details.
- It tries to find a record where
- This handles scenarios where multiple callbacks might arrive for the same message (e.g.,
queuedthensentthendelivered). TherawPayloadfield stores the entire JSON object received from Plivo for reference.
6. Real-time Frontend Updates (WebSockets)
To push updates to a frontend, we use the WebSocket server. This logic was also added within the try...catch block of the /plivo/callback route in Step 3, after the database save:
// Inside the try block of app.post('/plivo/callback', ...) after prisma.upsert
// Broadcast via WebSocket
broadcast({ // Call the broadcast function defined earlier
type: 'status_update', // Add a type for client-side routing
messageUUID: messageUUID,
status: status,
errorCode: errorCode || null,
timestamp: savedStatus.updatedAt // Send the DB timestamp
});
console.log(`Broadcasted status update for ${messageUUID} via WebSocket`);- The
broadcastfunction (defined in Step 1) sends the status update data as a JSON string to all currently connected WebSocket clients. - We include a
typefield (status_update) so client-side code can differentiate message types.
Client-Side Implementation (Conceptual Example for React/Vue):
// Example React/Vue component using WebSocket
import React, { useState, useEffect } from 'react';
function StatusDashboard() {
const [statuses, setStatuses] = useState({}); // Store statuses by MessageUUID
const [connectionStatus, setConnectionStatus] = useState('Connecting...');
useEffect(() => {
// Determine WebSocket protocol based on window location
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Use the same host as the web page, default port if needed
const wsHost = window.location.host; // e.g., localhost:3000 or your-production-domain.com
const wsUrl = `${wsProtocol}//${wsHost}`; // Connects to the root path where WS server is attached
let ws;
let reconnectInterval;
function connect() {
ws = new WebSocket(wsUrl);
console.log('Attempting WebSocket connection to:', wsUrl);
setConnectionStatus('Connecting...');
ws.onopen = () => {
console.log('WebSocket connected');
setConnectionStatus('Connected');
if (reconnectInterval) {
clearInterval(reconnectInterval);
reconnectInterval = null;
}
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('Received status update:', data);
if (data.type === 'status_update' && data.messageUUID) {
setStatuses(prevStatuses => ({
...prevStatuses,
[data.messageUUID]: {
status: data.status,
errorCode: data.errorCode,
timestamp: data.timestamp,
},
}));
} else if (data.message) {
console.log(""Server message:"", data.message); // Handle initial connection message etc.
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('Error');
ws.close(); // Ensure connection is closed on error before retry
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setConnectionStatus('Disconnected - Retrying...');
// Simple exponential backoff retry
if (!reconnectInterval) {
let delay = 1000;
reconnectInterval = setInterval(() => {
console.log(`Retrying connection in ${delay / 1000}s...`);
connect(); // Attempt to reconnect
delay = Math.min(delay * 2, 30000); // Increase delay up to 30s
}, delay);
}
};
}
connect(); // Initial connection attempt
// Cleanup function to close WebSocket and clear interval on component unmount
return () => {
if (reconnectInterval) {
clearInterval(reconnectInterval);
}
if (ws) {
ws.close();
}
};
}, []); // Empty dependency array ensures this runs only once on mount
return (
<div>
<h2>Live Message Statuses</h2>
<p>Connection: {connectionStatus}</p>
<ul>
{Object.entries(statuses).map(([uuid, data]) => (
<li key={uuid}>
<strong>UUID:</strong> {uuid} |
<strong>Status:</strong> {data.status}
{data.errorCode && ` (Error: ${data.errorCode})`} |
<strong>Updated:</strong> {new Date(data.timestamp).toLocaleString()}
</li>
))}
</ul>
{Object.keys(statuses).length === 0 && connectionStatus === 'Connected' && <p>Waiting for status updates...</p>}
</div>
);
}
export default StatusDashboard;- This frontend component connects to the WebSocket server running on the same host and port as the main HTTP server. It uses
ws:orwss:based on the page protocol. - It listens for messages, parses the JSON, and updates the component's state if it's a
status_update. - It includes basic connection status display and a simple reconnection mechanism.
- It displays the live status updates.
7. Sending a Test Message
To trigger the callback flow, we need an endpoint to send an SMS/MMS message using the Plivo SDK.
-
Implement Send Endpoint: Add the following route to
server.js.javascript// server.js // ... (imports, setup, other routes) ... // Endpoint to send a test SMS app.post('/send-test-sms', express.json(), async (req, res) => { // Ensure JSON parsing for this route const { to, text } = req.body; if (!to || !text) { return res.status(400).json({ error: 'Missing "to" (recipient number) or "text" in request body' }); } if (!process.env.PLIVO_PHONE_NUMBER) { console.error("Plivo sender phone number (PLIVO_PHONE_NUMBER) is not configured in .env"); return res.status(500).json({ error: 'Server configuration error: Missing sender number' }); } if (!process.env.BASE_CALLBACK_URL) { console.error("Base callback URL (BASE_CALLBACK_URL) is not configured in .env"); return res.status(500).json({ error: 'Server configuration error: Missing callback URL base' }); } const senderNumber = process.env.PLIVO_PHONE_NUMBER; // Construct the full callback URL for this specific message const deliveryStatusCallbackUrl = `${process.env.BASE_CALLBACK_URL}/plivo/callback`; console.log(`Attempting to send SMS from ${senderNumber} to ${to} with callback to ${deliveryStatusCallbackUrl}`); try { const response = await plivoClient.messages.create( senderNumber, // src to, // dst text, // text { url: deliveryStatusCallbackUrl, // Delivery status report URL method: 'POST' // Method for the callback } // For MMS, add 'media_urls': ['https://example.com/image.jpg'] ); console.log('Plivo Send API Response:', response); // The response contains the message_uuid which you can correlate // with the callbacks later. res.status(202).json({ message: 'Message sending initiated via Plivo.', plivoResponse: response }); } catch (error) { console.error('Error sending message via Plivo:', error); res.status(error.statusCode || 500).json({ error: 'Failed to send message via Plivo.', details: error.message || error.error || error // Plivo SDK might have different error structures }); } }); // ... (Global Error Handler, Server Start, Shutdown Logic) ...
Frequently Asked Questions
How to track Plivo SMS delivery status in Node.js?
Use Plivo's webhooks (callbacks) to receive real-time delivery status updates. Set up a Node.js (Express) backend to receive HTTP POST requests from Plivo containing message status information like 'queued', 'sent', 'delivered', or 'failed'.
What is the Plivo message status callback URL?
The callback URL is the publicly accessible URL of your Node.js server endpoint that will receive status updates. In development, use Ngrok to expose your local server, then provide the Ngrok HTTPS URL to Plivo as your `BASE_CALLBACK_URL`. In production, it's the public URL of your deployed application.
Why does Plivo callback validation matter?
Callback validation ensures that incoming requests genuinely originate from Plivo and haven't been tampered with. This prevents unauthorized access and protects your application from malicious attacks.
When should I acknowledge Plivo's callback request?
Acknowledge Plivo's callback immediately with a 200 OK response before performing any database operations or sending WebSocket updates. This prevents Plivo from retrying the callback due to timeouts, ensuring reliable notification delivery.
Can I push Plivo status updates to a frontend?
Yes, you can implement real-time status updates in your frontend (e.g., React/Vue) by setting up a WebSocket server in your Node.js backend. Broadcast status updates to connected clients after receiving and processing a valid Plivo callback.
How to send a test SMS message with Plivo?
Create a POST endpoint in your Node.js Express app that uses the Plivo Node.js SDK's `messages.create()` method. Provide your Plivo phone number, the recipient's number, the message text, and the callback URL for delivery updates.
How to set up a Plivo callback endpoint in Express.js?
Use `app.post('/plivo/callback', validatePlivoSignature, callbackHandler)` to define your route. The `validatePlivoSignature` middleware validates requests, and `callbackHandler` processes and saves the status updates to the database.
What are common Plivo SMS message statuses?
Common statuses include 'queued', 'sent', 'delivered', 'failed', and 'undelivered'. 'Failed' indicates an issue on Plivo's end, while 'undelivered' indicates a carrier-side problem. `ErrorCode` provides details for failures.
How to validate Plivo callback signatures in Node?
Use `plivo.validateV3Signature` method from the Plivo Node SDK, including the request method, full URL, nonce, timestamp, your Plivo Auth Token, provided signature, and the raw request body string.
How to store Plivo message statuses?
Use Prisma ORM (or your preferred database library) and a PostgreSQL (or your preferred) database. The provided example creates a `MessageStatus` table to record the unique message UUID, current status, error codes, the full callback payload, and timestamps.
What is the purpose of the messageUUID in Plivo callbacks?
The `messageUUID` is a unique identifier assigned by Plivo to each message you send. Use it to track individual messages, update status records in your database, and correlate callbacks to sent messages.
What Node.js libraries are used for Plivo callbacks?
The key libraries are `express` for the web server, `plivo` for the Plivo Node.js SDK, `dotenv` to load environment variables, and `ws` to implement a WebSocket server for real-time updates to the frontend (optional).
How to handle multiple Plivo callbacks for one message?
Use an upsert operation in your database logic. This allows you to update the existing record for the `messageUUID` if one exists, or create a new one if it doesn't, handling multiple status updates for a single message.
What technologies are used in this Plivo callback project?
The project utilizes Node.js with Express, the Plivo Node SDK, PostgreSQL database, Prisma ORM, optionally WebSockets with the 'ws' library, and Ngrok for local development testing.