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