code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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 .env file, 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:

  1. 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).
  2. Acknowledge: Plivo acknowledges the request, providing a unique MessageUUID.
  3. Deliver SMS: Plivo attempts to deliver the SMS to the recipient's handset.
  4. Receive Status: The recipient's carrier network informs Plivo about the delivery status (e.g., delivered, failed).
  5. Send Callback: Plivo sends an HTTP POST request to your specified callback URL, containing the MessageUUID, delivery Status, and other details.
  6. Verify Signature: Your application verifies the X-Plivo-Signature-V2 header to ensure the request genuinely came from Plivo.
  7. Respond OK: Your application responds with a 200 OK status to acknowledge receipt of the callback.
  8. Log Status: Your application logs the delivery status associated with the MessageUUID for monitoring or further processing.

Prerequisites:


1. Setting Up the Project

Let's create the project directory, initialize it, and install the necessary dependencies.

  1. Create Project Directory: Open your terminal or command prompt and create a new directory for your project.

    bash
    mkdir plivo-sms-callbacks
    cd plivo-sms-callbacks
  2. Initialize Node.js Project: This creates a package.json file to manage your project's dependencies and scripts.

    bash
    npm init -y

    (The -y flag accepts the default settings)

  3. Install Dependencies: We need Express for the web server, the Plivo SDK, and dotenv for managing environment variables.

    bash
    npm install express plivo-node dotenv
  4. Set Up Environment Variables: Create a file named .env in 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:3000
    • PLIVO_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 with ngrok, you'll update this later.
  5. Create .gitignore: Ensure your .env file and node_modules directory are not committed to Git. Create a file named .gitignore:

    plaintext
    # .gitignore
    node_modules/
    .env
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
  6. 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.

  1. Create sendSms.js: Create a file named sendSms.js in 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);
  2. Explanation:

    • require('dotenv').config();: Loads variables from your .env file into process.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 deliveryReportUrl using the BASE_URL from .env and 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 the POST HTTP method to send data to your url. POST is 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.
  3. 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.

    bash
    node 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.

  1. Create server.js: Create a file named server.js in 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)`);
    });
  2. Explanation:

    • Middleware:
      • express.urlencoded({ extended: true }): Parses incoming requests with Content-Type: application/x-www-form-urlencoded. The extended: true option 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-V2 and X-Plivo-Signature-V2-Nonce headers sent by Plivo, along with the PLIVO_AUTH_TOKEN from 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), and req.originalUrl.
        • It creates the baseString by concatenating the full URL and the nonce.
        • It calculates the expected signature using crypto.createHmac('sha256', ...) with your PLIVO_AUTH_TOKEN and the baseString.
        • It compares the calculated signature with the one received from Plivo using crypto.timingSafeEqual (important to prevent timing attacks). A try...catch block handles potential errors during comparison (e.g., invalid base64), and an explicit length check is added before timingSafeEqual.
        • If the signatures match, it calls next() to proceed to the route handler. Otherwise, it sends a 403 Forbidden or 400 Bad Request response.
    • 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 verifyPlivoSignature middleware before the main handler.
        • The handler logs the received data (req.body), which includes Status, MessageUUID, From, To, and potentially ErrorCode.
        • Important: It sends back a 200 OK status 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.

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.

  1. Start Your Local Server: Open a terminal in your project directory and run:

    bash
    node server.js

    You should see Server listening on port 3000....

  2. Start ngrok: Open a second terminal window (leave the server running) and start ngrok, telling it to forward to the port your server is running on (e.g., 3000).

    bash
    ngrok http 3000
  3. Get Your Public ngrok URL: ngrok will 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:3000

    Copy the https:// forwarding URL (e.g., https://<random_string>.ngrok.io). This is your temporary public URL. Using HTTPS is strongly recommended.

  4. Update .env: Go back to your .env file and update the BASE_URL with your public ngrok HTTPS 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
  5. Restart Your Server (Important): Stop your Node.js server (Ctrl+C in the first terminal) and restart it (node server.js) after changing the .env file to ensure it picks up the new BASE_URL.

  6. Send Another Test SMS: Now, run the sendSms.js script again with a valid recipient number.

    bash
    node sendSms.js <your_recipient_phone_number> ""Test message with callback""
  7. Verify Callback:

    • Node Server Logs: Watch the terminal where node server.js is running. Within a few seconds to minutes (depending on the carrier), you should see the log output from the /sms/delivery-report route, including ""Plivo signature verified successfully."", the Status (e.g., delivered, sent, failed, undelivered), and MessageUUID.
    • ngrok Logs: The terminal running ngrok will show incoming POST /sms/delivery-report requests with a 200 OK response. You can also inspect requests in detail via the ngrok web interface (usually http://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 OK status.

5. Error Handling, Logging, and Retry Mechanisms

  • Error Handling:
    • The sendSms.js script includes basic try...catch around 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.js handles 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 a 2xx code to Plivo unless the request itself was malformed (4xx). Avoid 5xx responses if possible, as Plivo might retry aggressively.
  • Logging:
    • We are using console.log for simplicity. For production, use a more robust logging library like winston or pino.
    • 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).
  • Retry Mechanisms:
    • Sending: If sendSms.js fails due to a temporary network issue or a Plivo server error (5xx), you might implement a simple retry logic (e.g., using async-retry) with exponential backoff.
    • Receiving Callbacks: Plivo handles retrying callbacks if your endpoint doesn't respond with 2xx within 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 unique MessageUUID).

6. Database Schema and Data Layer (Conceptual)

While this guide doesn't implement a database, here's how you would typically integrate it:

  1. 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);
  2. Integration:

    • In sendSms.js: After successfully calling client.messages.create, insert a new record into sms_messages with the messageUuid, recipient, sender, text, initial response, and set status to 'queued' or 'sent' based on the API response.
    • In server.js (/sms/delivery-report):
      • Extract the MessageUUID and Status (and ErrorCode if present) from req.body.
      • Find the corresponding record in sms_messages using the MessageUUID.
      • Update the status, status_timestamp, and error_code for that record.
      • Use a database transaction if you need to perform multiple updates atomically.
  3. 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 .env files out of version control.

  • HTTPS: Always use HTTPS for your callback URL (ngrok provides 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.js comes 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 like express-rate-limit to 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.create response might contain multiple messageUuids. The delivery report callback will typically be sent per segment, each with its corresponding MessageUUID. 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 Status values (queued, sent, delivered, failed, undelivered) and any associated ErrorCode values. See Plivo documentation for details. Build your application logic accordingly.
  • Callback Timeouts: Ensure your /sms/delivery-report endpoint responds quickly (within Plivo's timeout, typically ~5 seconds, ideally under 1-2 seconds) with a 200 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-report handler 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/await for I/O operations (like database calls, though ideally offloaded).
  • Database Indexing: Ensure message_uuid is indexed (ideally primary key) for fast lookups when updating status. Index other commonly queried fields like status or recipient_number.
  • Load Testing: Use tools like k6, artillery, or autocannon to test how many concurrent callbacks your server can handle.
  • Node.js Clustering: For high throughput, run your Node.js application using the built-in cluster module or a process manager like pm2 in cluster mode to utilize multiple CPU cores.

10. Monitoring, Observability, and Analytics

  • Health Checks: The /health endpoint 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-report handler).
    • Error rate of the callback endpoint.
    • Rate of signature verification failures.
  • Alerting: Set up alerts based on metrics and logs:
    • High rate of failed/undelivered statuses.
    • High callback endpoint error rate (>1%).
    • High callback processing latency.
    • Significant number of signature verification failures.
    • Health check failures.

11. Troubleshooting and Caveats

  • Incorrect Auth ID/Token: Leads to 401 Unauthorized errors when sending SMS or signature verification failures. Double-check credentials in .env and the Plivo console.
  • Invalid src (Sender ID): Ensure the PLIVO_SENDER_ID is 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. Use ngrok for local testing or deploy to a public server.
    • Incorrect URL in sendSms: Ensure the url parameter matches the endpoint route in server.js and uses the correct BASE_URL. Check for typos.
    • HTTP vs HTTPS: Use HTTPS for ngrok and production URLs. Plivo may not send callbacks to HTTP URLs.
    • Server Not Running: Ensure node server.js is running when expecting callbacks.
    • Firewall Blocking: Server firewalls might block incoming requests from Plivo's IP range.
  • 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 return 200 OK.
  • Signature Verification Failure:
    • Check PLIVO_AUTH_TOKEN is correct in your .env file 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. Check ngrok inspector or server logs for the exact URL Plivo called. Proxies or non-standard ports can sometimes alter hostname/protocol headers. Using req.get('host') is generally more robust than req.hostname.
    • Verify the correct nonce and signature headers (X-Plivo-Signature-V2, X-Plivo-Signature-V2-Nonce) are being read and used.
  • Body Parsing Issues: Ensure express.urlencoded({ extended: true }) middleware is used before your route handler if the Content-Type is application/x-www-form-urlencoded (which is typical for Plivo callbacks). If Plivo were sending JSON (application/json), you would use express.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.