code examples
code examples
Build Two-Way SMS with Plivo and Fastify: Complete Node.js Guide
Learn how to build production-ready two-way SMS messaging with Plivo and Fastify. Includes webhook handling, automatic replies, signature validation, and database integration.
Building Two-Way SMS Messaging with Plivo and Fastify
Two-way SMS messaging lets you receive messages from users and respond automatically – essential for customer support, appointment confirmations, surveys, and interactive campaigns. Plivo provides the SMS infrastructure, while Fastify delivers a lightweight, high-performance Node.js framework perfect for handling webhook requests.
This guide shows you how to build a production-ready two-way SMS system with Plivo and Fastify. You'll implement inbound message handling, automatic replies, database persistence, security features, and error handling.
What You'll Build
By the end of this tutorial, you'll have a working application that:
- Receives incoming SMS messages via Plivo webhooks
- Replies automatically using Plivo XML responses
- Validates webhook signatures for security
- Logs all messages with structured logging
- Handles errors gracefully with retries
- Stores message history in a database (optional)
- Rate-limits requests to prevent abuse
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).
ngrokinstalled 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.
bashmkdir fastify-plivo-sms cd fastify-plivo-sms -
Initialize Node.js Project: This creates a
package.jsonfile to manage dependencies and project metadata.bashnpm init -y -
Install Dependencies: We need
fastifyfor the web server,plivofor the Plivo Node.js SDK,dotenvto handle environment variables, and@fastify/formbodyto parse thex-www-form-urlencodeddata sent by Plivo webhooks.bashnpm install fastify plivo dotenv @fastify/formbody -
Create Project Structure: A simple structure keeps things organized.
bashmkdir src touch src/server.js touch .env touch .gitignore -
Configure
.gitignore: Prevent committing sensitive files and unnecessary directories. Add the following to your.gitignorefile:text# 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.envfile, replacing placeholders later:dotenv# .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 ownPORT: 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 IDandAuth Tokenare 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..envfiles keep sensitive data separate from the codebase and prevent accidental commits to version control.dotenvloads 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-prettyis used for development clarity. @fastify/formbodyRegistration: 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
POSTroute that Plivo will call when an SMS is received. - We log the incoming
request.bodyfor debugging. - We extract
From,To, andTextfields (note the capitalization matching Plivo's payload). - Input Validation: Basic checks ensure the required fields are present. We also check if the
Tonumber 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-Typeheader toapplication/xmland send the generated XML with a200 OKstatus. Plivo expects this format to process the reply instructions.
- We define a
- Health Check: A simple
/healthendpoint is added as a best practice for monitoring. - Server Start: The standard Fastify
listenfunction starts the server, listening on the configuredPORTand0.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,Carriermight be included)
Successful Response (to Plivo):
- Status Code:
200 OK - Body: An XML document containing Plivo MessageXML instructions.
<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
.envfile: Ensure your.envfile has the correctPLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPLIVO_NUMBERobtained from your Plivo Console. -
Run the Fastify Server: Start your local application.
bashnode src/server.jsYou 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.
ngrokcreates a secure tunnel to your local machine.bash# If your server runs on port 3000 ngrok http 3000ngrokwill display output like this:textSession 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.00Copy the
https://<UNIQUE_ID>.ngrok-free.appURL (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
ngrokForwarding 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_NUMBERin 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 Requestfor invalid inputs. - XML Generation: The
try...catchblock 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...catcharound 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
leveloption (e.g.,'info','debug','error'). Use environment variables to set the log level in different environments (e.g.,debugin development,infoorwarnin production). - Structured Logging: Fastify's logger (Pino) outputs JSON by default (we used
pino-prettyfor 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
2xxresponse 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 OKand 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 (like5xxerrors) would be essential. Libraries likeasync-retrycan simplify this.
Testing Error Scenarios:
- Bad Input: Use
curlto send requests missingFrom,To, orTextto test the400response. - Internal Error: Temporarily modify the
server.jscode to throw an error inside thetryblock for the webhook handler to test the500response. - 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:
bashnpm install prisma @prisma/client --save-dev npx prisma init --datasource-provider sqliteThis creates a
prismadirectory with aschema.prismafile and updates.envwith aDATABASE_URL. -
Define Schema (
prisma/schema.prisma):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.
bashnpx prisma migrate dev --name initThis creates the SQLite database file (e.g.,
prisma/dev.db) and generates the Prisma Client. -
Use Prisma Client in
server.js:javascript// 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
FromandTonumbers against expected formats (e.g., E.164 using a library likelibphonenumber-jsif needed). - Sanitize the
Textinput 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-V3andX-Plivo-Signature-V3-Nonceheaders 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-V3andX-Plivo-Signature-V3-Nonceheaders. - 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
preParsingor 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 Forbiddenstatus) 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.bashnpm install @fastify/rate-limitjavascript// 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.
ngrokprovides this automatically for local testing. Ensure your production deployment uses HTTPS. -
Environment Variables: Keep
PLIVO_AUTH_TOKENand any other secrets out of your code and Git history. Use.envlocally 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
MediaContentTypesandMediaUrls. 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
MessageUUIDand 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:javascript// 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).
FAQ
How do I receive incoming SMS messages with Plivo and Fastify?
Configure a Plivo webhook URL pointing to your Fastify endpoint (e.g., /webhooks/plivo/messaging). Install @fastify/formbody to parse the application/x-www-form-urlencoded payload. Plivo sends inbound messages with parameters like From, To, Text, and MessageUUID. Your Fastify handler processes these and returns a Plivo XML response.
What is Plivo XML and how do I use it for SMS replies?
Plivo XML is a markup language for controlling SMS responses. Use plivo.Response() to create a response object, call addMessage(text, params) to add your reply, and use toXML() to generate the XML string. Return this XML with Content-Type: application/xml to automatically send the reply.
How do I validate Plivo webhook signatures in Node.js?
Use Plivo's V3 signature validation by extracting the X-Plivo-Signature-V3 header and X-Plivo-Signature-V3-Nonce from incoming requests. Pass these along with your Plivo Auth Token and the webhook URL to Plivo's validation function. This prevents unauthorized webhook requests from spoofed sources.
Can I use Fastify's built-in rate limiting with Plivo webhooks?
Yes, install @fastify/rate-limit and configure it to limit requests per IP address. Set appropriate limits (e.g., 100 requests per minute) to prevent webhook abuse while allowing legitimate traffic. Rate limiting protects your application from denial-of-service attacks and excessive API usage.
How do I test Plivo webhooks locally with ngrok?
Install ngrok and run ngrok http 3000 to create a public HTTPS URL tunneling to your local Fastify server. Copy the ngrok URL (e.g., https://abc123.ngrok.io), append your webhook path (e.g., /webhooks/plivo/messaging), and configure this full URL in your Plivo number settings.
What database should I use to store SMS messages with Fastify?
Prisma works well with Fastify for storing SMS history. Define a Message model with fields like messageUUID, from, to, text, direction, and timestamp. Use Prisma Client to save inbound messages and track conversation history. Prisma supports PostgreSQL, MySQL, SQLite, and other databases.
How do I handle errors and retries with Plivo webhooks?
Implement try-catch blocks in your webhook handler and log errors with Fastify's built-in Pino logger. For failed Plivo API calls, implement exponential backoff retry logic. Always return a 200 status code to Plivo to acknowledge receipt, even if internal processing fails, to prevent duplicate webhook deliveries.
What's the difference between Plivo XML responses and API-based replies?
Plivo XML responses are synchronous – your webhook returns XML immediately and Plivo sends the reply. API-based replies use client.messages.create() to send messages asynchronously via separate HTTP requests. XML responses have lower latency and are simpler for immediate replies, while API calls offer more flexibility for delayed or conditional responses.
How do I implement conversation context in two-way SMS with Fastify?
Store message history in a database indexed by phone number. When receiving an inbound message, query previous messages from that number to understand conversation context. Use this context to generate intelligent replies. Implement session timeouts (e.g., 30 minutes) to reset conversation state after periods of inactivity.
Can I send MMS messages with Plivo and Fastify?
Yes, Plivo supports MMS for US and Canadian numbers. Use the same webhook structure but check the Type parameter to distinguish SMS from MMS. Access media URLs via the Media parameter array. To send MMS replies, use the API method client.messages.create() with a media_urls array instead of XML responses, which only support text.
Frequently Asked Questions
What are best practices for error handling in Fastify Plivo apps?
Implement input validation and error checking for missing fields and XML generation issues. Use try-catch blocks and proper logging for debugging.
When should I consider asynchronous processing for Plivo webhooks?
If webhook processing takes more than a few seconds, acknowledge Plivo immediately and process asynchronously using a queue to avoid timeouts and retries.
How to build a two-way SMS app with Node.js and Fastify?
Use Node.js with Fastify for the backend, Plivo for SMS API, and ngrok for local development. This setup allows you to create a webhook endpoint that receives incoming messages and sends replies via Plivo.
What is the role of Plivo in a Fastify SMS application?
Plivo provides the SMS API and phone number for sending and receiving messages. It handles the webhook triggers that send incoming messages to your Fastify application.
Why use Fastify for a two-way SMS application?
Fastify's speed and efficiency make it ideal for handling real-time communication. Its low overhead and plugin architecture are well-suited for building performant web services.
When should I use ngrok with Plivo and Fastify?
Use ngrok during local development to create a public URL for Plivo webhooks. Plivo needs a public address to send incoming message data to your application.
Can I use a different database with Plivo and Fastify?
Yes, the example uses SQLite with Prisma, but you can use any database that suits your needs. The key is to store message data and manage conversation state, if required.
How to handle Plivo webhook requests in Fastify?
Create a POST route at '/webhooks/plivo/messaging'. Use the @fastify/formbody plugin to parse the x-www-form-urlencoded data from Plivo.
What is the purpose of the .env file in a Node.js and Plivo app?
The .env file securely stores sensitive information like your Plivo Auth ID, Auth Token, and phone number, keeping them out of your codebase.
How to send an SMS reply with Plivo in a Fastify app?
Use the Plivo Node.js SDK to construct a Plivo XML response. Set the 'src' to your Plivo number and 'dst' to the sender's number, and include the message text within the XML.
Why validate Plivo webhook requests in a Fastify SMS app?
Validating requests ensures that they come from Plivo and not malicious sources. Plivo offers signature validation to verify the authenticity of webhooks.
How to configure a Plivo application for a Fastify SMS app?
In the Plivo console, create an XML application, set the message URL to your ngrok URL + /webhooks/plivo/messaging, and select POST as the method.
What to do if my Fastify Plivo app receives duplicate messages?
Use the MessageUUID provided by Plivo to track messages. If using a database, enforce unique constraints on the MessageUUID to prevent processing duplicates.
How to handle STOP and HELP keywords in a Plivo SMS application?
Check the incoming message text for keywords like 'STOP' or 'HELP'. Implement logic to manage unsubscribes or provide help information as needed.