In today's connected world, real-time communication is crucial. SMS remains a ubiquitous channel for reaching users directly. This guide provides a complete walkthrough for building a production-ready two-way SMS messaging application using Node.js, the high-performance Fastify web framework, and the Plivo Communications Platform.
We will build a simple yet robust application that can receive incoming SMS messages sent to a Plivo phone number and automatically reply. This forms the foundation for more complex applications like customer support bots, notification systems, or SMS-based verification. We chose Fastify for its exceptional performance and developer-friendly features, Node.js for its asynchronous nature well-suited for I/O operations like API calls, and Plivo for its reliable SMS API and clear developer documentation.
Project Overview and Goals
Goal: Create a Node.js web service using Fastify that listens for incoming SMS messages via a Plivo webhook and sends an automated reply back to the sender.
Problem Solved: Enables applications to engage in two-way SMS conversations programmatically, handling incoming messages reliably and responding instantly.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Fastify: A fast, low-overhead web framework for Node.js.
- Plivo Node.js SDK: To easily interact with the Plivo API, specifically for generating reply XML.
- Plivo Account & Phone Number: Required for sending/receiving SMS and triggering webhooks.
- dotenv: To manage environment variables securely.
- ngrok (for local development): To expose the local Fastify server to the public internet for Plivo webhooks.
System Architecture:
+--------+ Sends SMS +------------+ POST Request +-----------------+ Generates Reply +------------+
| User | -----------------> | Plivo | --------------------> | Fastify Service | ------------------------> | Plivo |
| Device | | Platform | (Webhook URL) | (Node.js App) | (Using Plivo SDK) | Platform |
+--------+ Receives SMS +------------+ +-----------------+ +------------+
^ |
|-------------------------------------- Sends SMS Reply -------------------------------------------------------+
Prerequisites:
- Node.js (LTS version recommended, e.g., v18 or later) and npm/yarn.
- A Plivo account (Sign up for free).
- An SMS-enabled Plivo phone number (can be purchased via the Plivo console).
ngrok
installed for local development (Download ngrok).- Basic understanding of JavaScript, Node.js, APIs, and webhooks.
Final Outcome: A running Fastify application deployable to any Node.js hosting environment, capable of receiving and automatically replying to SMS messages sent to your configured Plivo number.
1. Setting up the Project
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, then navigate into it.
mkdir fastify-plivo-sms cd fastify-plivo-sms
-
Initialize Node.js Project: This creates a
package.json
file to manage dependencies and project metadata.npm init -y
-
Install Dependencies: We need
fastify
for the web server,plivo
for the Plivo Node.js SDK,dotenv
to handle environment variables, and@fastify/formbody
to parse thex-www-form-urlencoded
data sent by Plivo webhooks.npm install fastify plivo dotenv @fastify/formbody
-
Create Project Structure: A simple structure keeps things organized.
mkdir src touch src/server.js touch .env touch .gitignore
-
Configure
.gitignore
: Prevent committing sensitive files and unnecessary directories. Add the following to your.gitignore
file:# Dependencies node_modules/ # Environment variables .env* !.env.example # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db
-
Set up Environment Variables (
.env
): Plivo requires authentication credentials. We'll store these securely in environment variables. Add the following to your.env
file, replacing placeholders later:# .env PORT=3000 PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_NUMBER=YOUR_PLIVO_PHONE_NUMBER # The SMS-enabled number you own
PORT
: The port your Fastify server will listen on.PLIVO_AUTH_ID
&PLIVO_AUTH_TOKEN
: Your Plivo API credentials.PLIVO_NUMBER
: The Plivo phone number that will receive messages and be used as the sender for replies.
How to find Plivo Credentials:
- Log in to your Plivo Console.
- Your
Auth ID
andAuth Token
are displayed prominently on the dashboard homepage. - Navigate to ""Phone Numbers"" > ""Your Numbers"" to find your SMS-enabled Plivo number(s). Ensure the number format includes the country code (e.g.,
+14155551212
).
Why
.env
? Storing credentials directly in code is a major security risk..env
files keep sensitive data separate from the codebase and prevent accidental commits to version control.dotenv
loads these variables intoprocess.env
.
2. Implementing Core Functionality (Receiving & Replying)
Now, let's write the Fastify server code to handle incoming Plivo webhooks.
src/server.js
:
// src/server.js
'use strict';
// Load environment variables from .env file
require('dotenv').config();
// Import dependencies
const Fastify = require('fastify');
const formBodyPlugin = require('@fastify/formbody');
const plivo = require('plivo');
// Validate essential environment variables
const requiredEnv = ['PORT', 'PLIVO_AUTH_ID', 'PLIVO_AUTH_TOKEN', 'PLIVO_NUMBER'];
for (const variable of requiredEnv) {
if (!process.env[variable]) {
console.error(`Error: Missing required environment variable ${variable}. Please check your .env file.`);
process.exit(1); // Exit if critical config is missing
}
}
// Initialize Fastify instance with logging enabled
const fastify = Fastify({
logger: {
level: 'info', // Default level
transport: {
target: 'pino-pretty', // Make logs more readable during development
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
});
// Register the formbody plugin to parse x-www-form-urlencoded request bodies
fastify.register(formBodyPlugin);
// --- Webhook Endpoint for Incoming Plivo Messages ---
fastify.post('/webhooks/plivo/messaging', async (request, reply) => {
// Log the incoming request body for debugging
request.log.info({ body: request.body }, 'Received Plivo webhook');
// Extract necessary data from the Plivo webhook payload
// Plivo sends data with capitalized keys
const fromNumber = request.body.From;
const toNumber = request.body.To; // Your Plivo number
const text = request.body.Text;
// --- Basic Input Validation ---
if (!fromNumber || !toNumber || !text) {
request.log.warn('Received incomplete Plivo webhook data');
// Send a 400 Bad Request response if essential data is missing
reply.code(400).send('Missing required fields: From, To, or Text');
return;
}
// Ensure the message is directed to the configured Plivo number
// (Optional but recommended for multi-number setups)
if (toNumber !== process.env.PLIVO_NUMBER) {
request.log.warn(`Message received for incorrect number: ${toNumber}. Expected: ${process.env.PLIVO_NUMBER}`);
// Decide how to handle this - ignore, reply with error, etc.
// For now, we'll just log and send a generic success to Plivo to avoid retries
reply.code(200).send();
return;
}
request.log.info(`Message received - From: ${fromNumber}, To: ${toNumber}, Text: ${text}`);
// --- Generate the Reply using Plivo XML ---
try {
// Create a Plivo Response object
const response = new plivo.Response();
// Define the parameters for the reply message
const params = {
src: toNumber, // The reply comes FROM your Plivo number
dst: fromNumber, // The reply goes TO the original sender
};
// The content of your reply message
const replyText = `Thanks for your message! You said: ""${text}""`;
// Add a <Message> element to the XML response
response.addMessage(replyText, params);
// Convert the response object to XML
const xmlResponse = response.toXML();
request.log.info({ xml: xmlResponse }, 'Generated Plivo XML response');
// --- Send the XML Response to Plivo ---
// Set the correct Content-Type header for Plivo
reply.header('Content-Type', 'application/xml');
reply.code(200).send(xmlResponse);
} catch (error) {
request.log.error({ err: error }, 'Error generating Plivo XML response');
// Send a 500 Internal Server Error if something goes wrong during XML generation
reply.code(500).send('Internal Server Error');
}
});
// --- Health Check Endpoint ---
fastify.get('/health', async (request, reply) => {
reply.code(200).send({ status: 'ok', timestamp: new Date().toISOString() });
});
// --- Start the Server ---
const start = async () => {
try {
const port = parseInt(process.env.PORT || '3000', 10);
await fastify.listen({ port: port, host: '0.0.0.0' }); // Listen on all available network interfaces
fastify.log.info(`Server listening on port ${port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
Explanation:
- Dependencies & Config: We import
Fastify
,plivo
,@fastify/formbody
, and load.env
. We add crucial validation to ensure required environment variables are set. - Fastify Initialization: We create a Fastify instance with built-in logging enabled.
pino-pretty
is used for development clarity. @fastify/formbody
Registration: This middleware is essential because Plivo sends webhook data asapplication/x-www-form-urlencoded
, not JSON. This plugin parses that data and makes it available inrequest.body
.- Webhook Route (
/webhooks/plivo/messaging
):- We define a
POST
route that Plivo will call when an SMS is received. - We log the incoming
request.body
for debugging. - We extract
From
,To
, andText
fields (note the capitalization matching Plivo's payload). - Input Validation: Basic checks ensure the required fields are present. We also check if the
To
number matches our configuredPLIVO_NUMBER
. - Plivo Response Object: We create
new plivo.Response()
. - Reply Parameters: We set
src
(source) to our Plivo number anddst
(destination) to the sender's number. addMessage
: We add a<Message>
element to the XML response, specifying the reply text and parameters.toXML()
: We generate the final XML string required by Plivo.- Sending Response: Critically, we set the
Content-Type
header toapplication/xml
and send the generated XML with a200 OK
status. Plivo expects this format to process the reply instructions.
- We define a
- Health Check: A simple
/health
endpoint is added as a best practice for monitoring. - Server Start: The standard Fastify
listen
function starts the server, listening on the configuredPORT
and0.0.0.0
(to be accessible outside localhost, important for ngrok and deployment). Error handling ensures the application exits gracefully if the server fails to start.
3. The API Layer (Webhook)
In this application, the primary API interaction point is the webhook endpoint /webhooks/plivo/messaging
that Plivo calls.
Endpoint Documentation:
- URL:
/webhooks/plivo/messaging
- Method:
POST
- Content-Type (Request from Plivo):
application/x-www-form-urlencoded
- Content-Type (Response to Plivo):
application/xml
- Authentication: None directly on the endpoint (security relies on the obscurity of the URL and potentially Plivo's request signature validation - see Section 7).
Request Body Parameters (from Plivo):
Plivo sends various parameters. The key ones we use are:
From
: The phone number of the sender (e.g.,+12125551234
).To
: Your Plivo phone number that received the message (e.g.,+14155551212
).Text
: The content of the SMS message (e.g.,Hello World
).MessageUUID
: A unique identifier for the incoming message.- (Other parameters like
Type
,Carrier
might be included)
Successful Response (to Plivo):
- Status Code:
200 OK
- Body: An XML document containing Plivo MessageXML instructions.
<!-- Example Successful Response Body -->
<Response>
<Message src=""+14155551212"" dst=""+12125551234"">Thanks for your message! You said: ""Hello World""</Message>
</Response>
Error Responses (to Plivo):
- Status Code:
400 Bad Request
(if input validation fails)- Body: Text or JSON description of the error.
- Status Code:
500 Internal Server Error
(if XML generation or other server logic fails)- Body: Text or JSON description of the error.
Note: Plivo might retry sending the webhook if it doesn't receive a 2xx
response within its timeout period.
Testing with curl
(Simulating Plivo):
You can simulate Plivo's webhook call using curl
once your server is running locally (we'll use ngrok in the next step to make it accessible).
# Make sure your server is running: node src/server.js
# In another terminal, send a POST request simulating Plivo:
curl -X POST \
http://localhost:3000/webhooks/plivo/messaging \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode ""From=+12125551234"" \
--data-urlencode ""To=+14155551212"" \
--data-urlencode ""Text=Hello from curl!"" \
--data-urlencode ""MessageUUID=abc-123-def-456""
# Expected Output (XML):
# <?xml version=""1.0"" encoding=""utf-8""?><Response><Message dst=""+12125551234"" src=""+14155551212"">Thanks for your message! You said: ""Hello from curl!""</Message></Response>
Replace the phone numbers with appropriate test values (ensure the To
number matches your .env
file if you kept that validation).
4. Integrating with Plivo
Now we connect our running Fastify application to the Plivo platform.
-
Fill
.env
file: Ensure your.env
file has the correctPLIVO_AUTH_ID
,PLIVO_AUTH_TOKEN
, andPLIVO_NUMBER
obtained from your Plivo Console. -
Run the Fastify Server: Start your local application.
node src/server.js
You should see output indicating the server is listening on port 3000 (or your configured port).
-
Expose Local Server with ngrok: Plivo needs a publicly accessible URL to send webhooks.
ngrok
creates a secure tunnel to your local machine.# If your server runs on port 3000 ngrok http 3000
ngrok
will display output like this:Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Forwarding https://<UNIQUE_ID>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
Copy the
https://<UNIQUE_ID>.ngrok-free.app
URL (your URL will have a different unique ID). This is your public webhook URL for now. Keep ngrok running. -
Configure Plivo Application: We need to tell Plivo where to send incoming messages for your number. This is done via a Plivo Application.
- Go to your Plivo Console.
- Navigate to ""Messaging"" -> ""Applications"" -> ""XML"".
- Click the ""Add New Application"" button.
- Application Name: Give it a descriptive name (e.g.,
Fastify SMS Responder
). - Message URL: Paste your
ngrok
Forwarding URL, adding the specific webhook path:https://<UNIQUE_ID>.ngrok-free.app/webhooks/plivo/messaging
- Method: Select
POST
. - Answer URL / Hangup URL / Fallback Answer URL: Leave these blank (they are for voice applications).
- Click ""Create Application"".
-
Link Plivo Number to the Application: Assign the Plivo Application to your SMS-enabled phone number.
- Navigate to ""Phone Numbers"" -> ""Your Numbers"".
- Find the Plivo number you want to use (ensure it's the same as
PLIVO_NUMBER
in your.env
). Click on it. - In the ""Number Configuration"" section, find the ""Application Type"" dropdown. Select
XML Application
. - In the ""Plivo Application"" dropdown that appears, select the application you just created (
Fastify SMS Responder
). - Click ""Update Number"".
Your Fastify application is now configured to receive messages sent to your Plivo number!
5. Error Handling, Logging, and Retries
Error Handling:
- Input Validation: As shown in
server.js
, we check for the presence of essential fields (From
,To
,Text
). Return400 Bad Request
for invalid inputs. - XML Generation: The
try...catch
block aroundplivo.Response()
handles errors during XML creation (e.g., invalid parameters). Return500 Internal Server Error
. - Network/Plivo API Errors (for sending): While this guide focuses on receiving, if you extended it to send messages proactively using
client.messages.create()
, you would needtry...catch
around that call and handle potential Plivo API errors (e.g., authentication failure, invalid number format, insufficient funds). - Uncaught Exceptions: Use process listeners for uncaught exceptions and unhandled rejections, although Fastify handles many errors within its request lifecycle.
// Example: Add near the end of server.js before start()
process.on('uncaughtException', (err, origin) => {
fastify.log.fatal({ err, origin }, 'Uncaught Exception');
process.exit(1); // Exit gracefully after logging
});
process.on('unhandledRejection', (reason, promise) => {
fastify.log.fatal({ reason, promise }, 'Unhandled Rejection');
process.exit(1);
});
Logging:
- Fastify Logger: We initialized Fastify with
logger: true
. Userequest.log.info()
,request.log.warn()
,request.log.error()
within route handlers for contextual logging. - Log Levels: Control verbosity via the
level
option (e.g.,'info'
,'debug'
,'error'
). Use environment variables to set the log level in different environments (e.g.,debug
in development,info
orwarn
in production). - Structured Logging: Fastify's logger (Pino) outputs JSON by default (we used
pino-pretty
for development). JSON logs are easily parsed by log management systems (e.g., Datadog, Logstash, Splunk). - Key Information: Log relevant data like
MessageUUID
,From
,To
, and any error details. Avoid logging sensitive information unless necessary and properly secured.
Retry Mechanisms:
- Incoming Webhooks (Plivo to Your App): Plivo automatically retries sending webhooks if it doesn't receive a
2xx
response within a specific timeout (usually several seconds). Ensure your webhook responds quickly (ideally under 2-3 seconds) to avoid unnecessary retries. If your processing takes longer, acknowledge the request immediately with200 OK
and process asynchronously (using queues like RabbitMQ or Redis). - Outgoing API Calls (Your App to Plivo): If you were sending messages via
client.messages.create()
, implementing retries with exponential backoff for transient network errors or temporary Plivo issues (like5xx
errors) would be essential. Libraries likeasync-retry
can simplify this.
Testing Error Scenarios:
- Bad Input: Use
curl
to send requests missingFrom
,To
, orText
to test the400
response. - Internal Error: Temporarily modify the
server.js
code to throw an error inside thetry
block for the webhook handler to test the500
response. - Plivo Errors: Check Plivo's Debug Logs in the console (""Logs"" > ""Messaging Logs"") to see delivery statuses, webhook failures, and error codes returned by your application.
6. Creating a Database Schema and Data Layer (Optional)
For this simple echo bot, a database isn't strictly necessary. However, for real-world applications, you'll likely want to store message history, conversation state, or user information.
Here's a conceptual example using Prisma (a popular Node.js ORM) with SQLite (for simplicity).
-
Install Prisma:
npm install prisma @prisma/client --save-dev npx prisma init --datasource-provider sqlite
This creates a
prisma
directory with aschema.prisma
file and updates.env
with aDATABASE_URL
. -
Define Schema (
prisma/schema.prisma
):// prisma/schema.prisma generator client { provider = ""prisma-client-js"" } datasource db { provider = ""sqlite"" url = env(""DATABASE_URL"") // Uses the URL from .env } model Message { id String @id @default(cuid()) // Unique message ID (using CUID) plivoUuid String @unique // Plivo's MessageUUID direction String // ""inbound"" or ""outbound"" fromNumber String toNumber String text String? // Message content (optional if only media) status String? // Optional: Plivo status (e.g., delivered, failed) timestamp DateTime @default(now()) // When the record was created plivoTimestamp DateTime? // Optional: Timestamp from Plivo if available @@index([plivoUuid]) @@index([fromNumber]) @@index([toNumber]) @@index([timestamp]) } // Optional: Model for conversations if needed // model Conversation { // id String @id @default(cuid()) // userNumber String @unique // The external user's number // // Add other conversation state fields // createdAt DateTime @default(now()) // updatedAt DateTime @updatedAt // }
-
Apply Schema Migrations: Create and apply the database schema.
npx prisma migrate dev --name init
This creates the SQLite database file (e.g.,
prisma/dev.db
) and generates the Prisma Client. -
Use Prisma Client in
server.js
:// src/server.js - Add near the top const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // --- Inside the webhook handler, after extracting data --- try { // Store the inbound message await prisma.message.create({ data: { plivoUuid: request.body.MessageUUID || `fallback-${Date.now()}`, // Use Plivo UUID or generate one direction: 'inbound', fromNumber: fromNumber, toNumber: toNumber, text: text, // plivoTimestamp: request.body.Timestamp ? new Date(request.body.Timestamp) : null // Adjust based on Plivo actual payload }, }); request.log.info('Stored inbound message'); // --- Generate the Reply using Plivo XML --- // ... (rest of the Plivo response generation) ... // After successfully generating XML, potentially store the outbound reply record // Note: You don't know the Plivo UUID for the *reply* yet. // You might store a placeholder and update it later via Delivery Reports. // await prisma.message.create({ ... }); } catch (error) { // Handle Prisma errors specifically if needed if (error.code === 'P2002') { // Example: Unique constraint violation request.log.warn({ err: error }, 'Potential duplicate message based on Plivo UUID'); // Decide how to handle duplicates - maybe just acknowledge Plivo? reply.code(200).send(); // Acknowledge Plivo even if DB fails, prevents retries return; } request.log.error({ err: error }, 'Error processing message or DB operation'); reply.code(500).send('Internal Server Error'); return; // Ensure we don't continue if DB fails before reply logic } finally { // Ensure Prisma client is disconnected on app shutdown (add shutdown hooks) } // --- Add shutdown hook before start() --- const gracefulShutdown = async (signal) => { fastify.log.info(`Received ${signal}. Shutting down gracefully...`); await fastify.close(); await prisma.$disconnect(); fastify.log.info('Server and DB connection closed.'); process.exit(0); }; process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown);
This provides basic persistence. Real applications would need more sophisticated state management, conversation tracking, and handling of Plivo delivery reports to update message statuses.
7. Adding Security Features
Securing your webhook endpoint is crucial.
-
Input Validation and Sanitization:
- We already added basic validation for required fields.
- Validate
From
andTo
numbers against expected formats (e.g., E.164 using a library likelibphonenumber-js
if needed). - Sanitize the
Text
input if you plan to use it in database queries directly (Prisma helps prevent SQL injection) or display it in other contexts. Libraries likeDOMPurify
(for HTML) or simple regex replacements can help. For an echo bot, this is less critical.
-
Plivo Request Validation (Highly Recommended): Plivo can sign its webhook requests, allowing you to verify they genuinely originated from Plivo. This is a critical security measure for production applications.
-
Enable Signature Validation in Plivo Application: In the Plivo console, edit your application and check the box for signature validation (V3 recommended). Plivo will then send
X-Plivo-Signature-V3
andX-Plivo-Signature-V3-Nonce
headers with each request. -
Implementation: You need to use your Plivo Auth Token and the Plivo SDK's validation function (e.g.,
plivo.validateV3Signature
) within your webhook handler before processing the request. This typically involves:- Retrieving the
X-Plivo-Signature-V3
andX-Plivo-Signature-V3-Nonce
headers. - Reconstructing the full URL that Plivo called.
- Accessing the raw, unparsed request body. This is the most complex part with frameworks like Fastify that parse the body automatically. You often need to use a hook like
preParsing
or configure body parsing carefully to retain access to the raw buffer. - Calling the SDK's validation function with the method, URL, nonce, auth token, signature, and raw body.
- Rejecting the request (e.g., with a
403 Forbidden
status) if the signature is invalid.
- Retrieving the
-
Recommendation: Due to the complexities of accessing the raw request body alongside parsed data in Fastify, implementing this robustly requires careful attention to Fastify hooks and potentially custom body parsing configurations. Consult the official Plivo documentation and Node.js SDK examples for specific guidance on implementing V3 signature validation with Fastify. While this guide omits the specific code implementation for brevity and framework-specific complexity, adding signature validation is strongly advised for any production deployment. For now, security relies partly on the obscurity of the webhook URL.
-
-
Rate Limiting: Protect your endpoint from abuse or runaway scripts. Use
@fastify/rate-limit
.npm install @fastify/rate-limit
// src/server.js - Add near other require/registers const rateLimit = require('@fastify/rate-limit'); // Register rate limiting fastify.register(rateLimit, { max: 100, // Max requests per window (adjust based on expected load) timeWindow: '1 minute' // Time window // You can configure Redis for distributed rate limiting across multiple instances // keyGenerator: function (request) { /* ... */ }, // Rate limit per IP or Plivo number? });
-
HTTPS: Always use HTTPS for your webhook URL.
ngrok
provides this automatically for local testing. Ensure your production deployment uses HTTPS. -
Environment Variables: Keep
PLIVO_AUTH_TOKEN
and any other secrets out of your code and Git history. Use.env
locally and secure environment variable management in production (e.g., AWS Secrets Manager, Doppler, platform-specific env vars).
8. Handling Special Cases
-
Message Encoding: Plivo handles GSM and Unicode characters automatically. Long messages might be split (concatenated) by Plivo or the carrier; your reply logic doesn't usually need to worry about this unless you have strict length limits for replies. The
plivo.Response()
object handles XML generation correctly. -
Different Message Types (MMS): If you enable MMS on your Plivo number, the webhook payload will include
MediaContentTypes
andMediaUrls
. Your code would need to check for these fields and handle them accordingly (e.g., acknowledge receipt, download media). The reply mechanism remains the same (XML), but you might adjust the reply text. -
Rate Limits & Throttling: If you send many replies quickly, Plivo might throttle your outbound messages. Implement delays or use queues if sending high volumes. Plivo's API response for sending (
client.messages.create
) provides feedback. -
Duplicate Messages: Network issues can occasionally cause Plivo to send the same webhook multiple times. Using the
MessageUUID
and checking if you've already processed it (if using a database) can prevent duplicate replies or actions. Our optional database schema includes a unique constraint onplivoUuid
. -
Stop/Help Keywords: Carriers often require handling standard keywords like
STOP
,HELP
,INFO
. While Plivo offers some automated handling (configurable in the console), you might add logic to your webhook:// src/server.js - Inside webhook handler const upperText = text.toUpperCase().trim(); if (upperText === 'STOP') { request.log.info(`Received STOP request from ${fromNumber}`); // Optionally: Add number to an internal blocklist in your DB // Plivo might handle the opt-out automatically depending on settings. // Sending a confirming reply is often good practice, but check regulations. // const response = new plivo.Response(); // response.addMessage(""You have been unsubscribed. No more messages will be sent."", { src: toNumber, dst: fromNumber }); // reply.header('Content-Type', 'application/xml').code(200).send(response.toXML()); // OR just send 200 OK if Plivo handles it fully: reply.code(200).send(); return; } // Add similar logic for HELP, etc.
9. Performance Optimizations
For this simple application, Fastify's inherent speed is likely sufficient. However, for high-throughput scenarios:
- Asynchronous Processing: If any logic within the webhook (database lookups, external API calls) takes significant time (>1-2 seconds), acknowledge Plivo immediately with a
200 OK
(empty response or minimal XML) and perform the work asynchronously using a job queue (e.g., BullMQ with Redis, RabbitMQ). This prevents Plivo webhook timeouts and retries. - Database Indexing: Ensure database columns frequently used in queries (
plivoUuid
,fromNumber
,timestamp
) are indexed (as shown in the Prisma example). - Caching: If replies depend on frequently accessed, slow-to-retrieve data, implement caching (e.g., using Redis or in-memory caches like
fastify-caching
).