code examples
code examples
Plivo Node.js RedwoodJS: Build Inbound Two-Way SMS Messaging
Build two-way SMS messaging with Plivo and RedwoodJS. Complete guide covering webhook setup, signature validation, database integration, and automated replies.
This guide provides a complete walkthrough for building a RedwoodJS application capable of receiving incoming SMS messages via Plivo and responding automatically, enabling two-way SMS communication. We'll cover everything from initial project setup and Plivo configuration to database integration, security best practices, deployment, and troubleshooting.
By the end of this tutorial, you will have a RedwoodJS application with an API endpoint that acts as a webhook for Plivo. This endpoint will receive SMS messages sent to your Plivo number, log them to a database, and send an automated reply back to the sender. This forms the foundation for building more complex SMS-based features like customer support bots, notification systems, or interactive services.
<!-- GAP: Missing cost estimates for Plivo usage (Type: Substantive) -->Project Overview and Goals
Goal: To create a RedwoodJS application that can:
- Receive incoming SMS messages sent to a Plivo phone number.
- Process the message content (e.g., log it).
- Automatically send a reply back to the sender via SMS.
Problem Solved: Enables businesses to engage with users via SMS, providing automated responses or triggering backend processes based on received messages. It establishes a robust foundation for two-way SMS communication within a modern full-stack JavaScript application.
<!-- EXPAND: Could benefit from real-world use case examples (Type: Enhancement) -->Technologies Used:
- RedwoodJS: A full-stack, serverless web application framework built on React, GraphQL, and Prisma. Chosen for its integrated structure (API and web sides), developer experience, and seamless database integration with Prisma.
- Plivo: A cloud communications platform providing SMS and Voice APIs. Chosen for its reliable SMS delivery, developer-friendly API, and Node.js SDK.
- Node.js: The underlying runtime for the RedwoodJS API side.
- Prisma: A next-generation ORM for Node.js and TypeScript, used by RedwoodJS for database access.
- PostgreSQL (or other Prisma-supported DB): For storing message logs.
System Architecture:
[User's Phone] <---- SMS ----> [Plivo Platform] <---- HTTPS Webhook ----> [RedwoodJS API Function]
^ | |
|--- SMS Reply ---------------| |--- Processes Request
| |--- Logs to Database (Prisma)
| |--- Generates Plivo XML Reply
| '--- Sends XML Response to Plivo
'------------------------------------------------------------------------'
- A user sends an SMS to your Plivo phone number.
- Plivo receives the SMS and sends an HTTP POST request (webhook) to the
message_urlyou configure. - Your RedwoodJS API function receives this webhook request.
- The function parses the incoming data (
From,To,Text). - (Optional but recommended) The function validates the webhook signature to ensure it came from Plivo.
- The function logs the incoming message details to your database using Prisma.
- The function constructs an XML response using the Plivo Node.js SDK, instructing Plivo to send a reply SMS.
- The function sends this XML back to Plivo with an HTTP 200 OK status.
- Plivo receives the XML and sends the specified reply SMS back to the original user.
Prerequisites:
- Node.js (v18 or later recommended) and Yarn installed.
- A Plivo account (Sign up for free).
- An SMS-enabled Plivo phone number. You can purchase one from the Plivo console under Phone Numbers > Buy Numbers.
- Access to a PostgreSQL database (or SQLite/MySQL/SQL Server, configured for Prisma).
ngrokinstalled for local development testing (Download ngrok).- Basic understanding of RedwoodJS concepts (API functions, services, Prisma).
How Do You Set Up a RedwoodJS Project for Plivo SMS?
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
- Create RedwoodJS App:
Open your terminal and run:
Choose your preferred database during setup (PostgreSQL is recommended for production). For this guide, we'll assume PostgreSQL.bash
yarn create redwood-app ./redwood-plivo-sms cd redwood-plivo-sms
- Install Plivo SDK:
The Plivo SDK is needed on the API side to interact with Plivo APIs and generate response XML.
bash
yarn workspace api add plivo
-
Environment Variables: Plivo requires an Auth ID and Auth Token for authentication. Never hardcode these in your source code. We'll use environment variables.
- Find your Plivo Auth ID and Auth Token on the Plivo Console dashboard.
- Create a
.envfile in the root of your RedwoodJS project (if it doesn't exist). - Add your Plivo credentials and your Plivo phone number to the
.envfile using the standardKEY=VALUEformat:
plaintext# .env # Plivo Credentials PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN # Your Plivo Phone Number (in E.164 format, e.g., +14155551212) PLIVO_NUMBER=+1XXXXXXXXXX # Database URL (Redwood generates this during setup) DATABASE_URL=postgresql://user:password@host:port/database?schema=public- Purpose: Storing sensitive credentials and configuration outside the codebase enhances security and makes configuration easier across different environments (development, staging, production).
PLIVO_NUMBERis stored here for easy reference when constructing replies or sending outbound messages.
- Git Initialization:
It's good practice to initialize Git early.
Make surebash
git init git add . git commit -m ""Initial project setup with RedwoodJS and Plivo SDK"".envis included in your.gitignorefile (RedwoodJS adds it by default).
How Do You Implement the Webhook Handler for Inbound SMS?
We need an API endpoint (a RedwoodJS function) that Plivo can call when an SMS is received. This function will process the incoming message and generate the reply.
-
Generate API Function: Use the RedwoodJS CLI to generate a new serverless function:
bashyarn rw g function plivoSmsWebhookThis creates
api/src/functions/plivoSmsWebhook.js. -
Implement Webhook Logic: Open
api/src/functions/plivoSmsWebhook.jsand replace its contents with the following code:
```javascript
// api/src/functions/plivoSmsWebhook.js
import { logger } from 'src/lib/logger'
import { db } from 'src/lib/db' // We'll use this later in Section 6
import plivo from 'plivo'
/**
* @param {import('@redwoodjs/api').ApiEvent} event - The incoming HTTP request event object. Contains headers, body, etc.
* @param {import('@redwoodjs/api').ApiContext} context - The context object for the function invocation.
*/
export const handler = async (event, context) => {
logger.info('Received Plivo SMS webhook request')
// Plivo sends data as application/x-www-form-urlencoded.
// RedwoodJS might parse common body types. We need to handle both string and object bodies.
let requestBody = {}
if (typeof event.body === 'string') {
try {
// Parse application/x-www-form-urlencoded data
requestBody = Object.fromEntries(new URLSearchParams(event.body))
} catch (e) {
logger.error('Failed to parse request body:', e)
return { statusCode: 400, body: 'Bad Request: Invalid body format' }
}
} else if (typeof event.body === 'object' && event.body !== null) {
// Assume RedwoodJS already parsed it (e.g., if Content-Type was application/json, though Plivo uses form-urlencoded)
requestBody = event.body
} else {
logger.error('Received event with unexpected body type:', typeof event.body)
return { statusCode: 400, body: 'Bad Request: Unexpected body format' }
}
const fromNumber = requestBody.From
const toNumber = requestBody.To // This is your Plivo number
const text = requestBody.Text
const messageUuid = requestBody.MessageUUID // Plivo's unique ID for the message
if (!fromNumber || !toNumber || !text || !messageUuid) {
logger.warn('Webhook received incomplete data:', requestBody)
// Still return 200 OK to Plivo to prevent retries, but don't process
const emptyResponse = new plivo.Response()
return {
statusCode: 200,
headers: { 'Content-Type': 'application/xml' },
body: emptyResponse.toXML(),
}
}
logger.info(`Message received - From: ${fromNumber}, To: ${toNumber}, Text: ${text}`)
// **TODO: Section 6 - Add database logging here**
// try {
// await db.smsMessage.create({ data: { ... } });
// } catch (dbError) {
// logger.error('Failed to save message to DB:', dbError);
// // Decide if you still want to reply even if DB save fails
// }
// **TODO: Section 7 - Add webhook signature validation here**
// const isValid = plivo.validateRequest(...)
// if (!isValid) { ... }
// --- Construct the Reply ---
const response = new plivo.Response()
// Simple auto-reply message
const replyText = `Thanks for your message! You said: ""${text}""`
const params = {
src: toNumber, // Reply FROM your Plivo number
dst: fromNumber, // Reply TO the sender's number
}
response.addMessage(replyText, params)
const xmlResponse = response.toXML()
logger.info('Sending XML Response to Plivo:', xmlResponse)
// --- Send Response to Plivo ---
return {
statusCode: 200, // IMPORTANT: Always return 200 OK if you successfully processed the request (even if you don't send a reply)
headers: {
'Content-Type': 'application/xml', // IMPORTANT: Plivo expects XML
},
body: xmlResponse, // The generated XML instructing Plivo to send the reply
}
}
```
* **Explanation:**
* We import Redwood's logger and Prisma client (`db`).
* The `handler` function receives the HTTP request (`event`).
* We parse the `event.body`, expecting form-urlencoded data from Plivo (`From`, `To`, `Text`, `MessageUUID`). `URLSearchParams` is used for parsing.
* We log the received message details.
* Placeholders are included for Database Logging (Section 6) and Security Validation (Section 7).
* We instantiate `plivo.Response()`.
* We define the reply message text.
* `response.addMessage(replyText, params)` adds a `<Message>` element to the XML response. `src` is your Plivo number (`toNumber` from the incoming request), and `dst` is the original sender's number (`fromNumber`).
* `response.toXML()` generates the final XML string.
* We return an HTTP 200 OK response with the `Content-Type` set to `application/xml` and the generated XML as the body. This tells Plivo to send the reply SMS.
* **Why XML?** Plivo's webhook mechanism uses XML (specifically Plivo XML or PHLO) to control communication flows. Returning specific XML elements instructs Plivo on the next action (e.g., send SMS, play audio, gather input).
<!-- GAP: Missing timeout handling explanation (Type: Substantive) -->
<!-- GAP: Missing maximum message length considerations (Type: Substantive) -->
3. Building a Complete API Layer
For this specific use case (receiving and replying to SMS via webhook), the API function plivoSmsWebhook.js is the primary API layer interacting directly with Plivo.
- Authentication/Authorization: The security of this endpoint relies on validating the webhook signature from Plivo (covered in Section 7), not typical user authentication. Anyone knowing the URL could potentially call it, hence the signature validation is critical.
- Request Validation: Basic validation (checking for
From,To,Text) is included. More complex business logic validation (e.g., checking user status based onfromNumber) would be added here or in a dedicated service. - API Endpoint Documentation:
- Endpoint:
/api/plivoSmsWebhook(relative to your deployed base URL) - Method:
POST(Typically, but Plivo allowsGETtoo – configure in Plivo App) - Request Body Format:
application/x-www-form-urlencoded - Request Parameters (from Plivo):
From: Sender's phone number (E.164 format)To: Your Plivo phone number (E.164 format)Text: The content of the SMS message (UTF-8 encoded)Type:smsMessageUUID: Plivo's unique identifier for the incoming messageEvent:message(for standard incoming messages)- (Other parameters may be included, see Plivo Incoming Message Webhook Docs)
- Success Response:
- Code:
200 OK - Content-Type:
application/xml - Body: Plivo XML, e.g.:
xml
<Response> <Message src=""+1XXXXXXXXXX"" dst=""+1YYYYYYYYYY"">Thanks for your message! You said: ""Hello""</Message> </Response>
- Code:
- Error Response: While you can return non-200 codes, Plivo might retry. It's often better to return
200 OKwith an empty<Response/>or log the error internally and potentially send an error SMS if appropriate. If signature validation fails, return403 Forbidden.
- Endpoint:
-
Testing with cURL/Postman: You can simulate Plivo's request locally (once
ngrokis running, see Section 4). Note: You will need to replace the placeholder values (YOUR_NGROK_URL,+1SENDERNUMBER,+1YOURPLIVONUMBER) with your actual ngrok URL and phone numbers.bash# Replace YOUR_NGROK_URL with the URL provided by ngrok # Replace +1SENDERNUMBER with a valid sender number (E.164 format) # Replace +1YOURPLIVONUMBER with your Plivo number (E.164 format) curl -X POST YOUR_NGROK_URL/api/plivoSmsWebhook \ -H ""Content-Type: application/x-www-form-urlencoded"" \ --data-urlencode ""From=+1SENDERNUMBER"" \ --data-urlencode ""To=+1YOURPLIVONUMBER"" \ --data-urlencode ""Text=Hello from cURL"" \ --data-urlencode ""MessageUUID=abc-123-def-456""You should receive the XML response back in the terminal.
How Do You Configure Plivo to Send Webhooks to RedwoodJS?
Now, let's configure Plivo to send webhooks to our local development server and then to our deployed application.
-
Start Local Development Server:
bashyarn rw devYour RedwoodJS app (including the API function) is now running, typically accessible via
http://localhost:8910for the web side andhttp://localhost:8911for the API/functions (check your terminal output). Our webhook is athttp://localhost:8911/plivoSmsWebhook. -
Expose Local Server with ngrok: Plivo needs a publicly accessible URL to send webhooks. Open a new terminal window and run:
bashngrok http 8911- Note: Port
8911is the default for RedwoodJS API functions. Verify this in yourredwood.tomlor dev server output if needed. ngrokwill display a Forwarding URL (e.g.,https://abcdef123456.ngrok.io). This URL tunnels requests to yourlocalhost:8911. Copy thehttpsversion of this URL.
- Note: Port
- Configure Plivo Application:
- Log in to the Plivo Console.
- Navigate to Messaging -> Applications -> XML.
- Click Add New Application.
- Application Name: Give it a descriptive name (e.g.,
Redwood Dev SMS Handler). - Message URL: Paste your
ngrokforwarding URL, appending the function path:https://abcdef123456.ngrok.io/api/plivoSmsWebhook - Method: Select
POST. - (Optional but Recommended) Fallback URL: You can set a URL to be called if your primary Message URL fails.
- (Optional but Recommended for Security) Auth ID / Auth Token: Leave these blank for now if you haven't implemented signature validation (Section 7). If you have implemented it, paste your Plivo Auth ID and Auth Token here so Plivo includes the signature header.
- Click Create Application.
-
Assign Application to Plivo Number:
- Navigate to Phone Numbers -> Your Numbers.
- Find the SMS-enabled Plivo number you want to use. Click on it.
- In the Number Configuration section, find the Application Type. Select
XML Application. - From the Plivo Application dropdown, select the application you just created (
Redwood Dev SMS Handler). - Click Update Number.
-
Test Locally:
- Send an SMS message from your mobile phone to your Plivo phone number.
- Watch the terminal running
yarn rw dev. You should see logs:INFO: Received Plivo SMS webhook requestINFO: Message received - From: +YOURMOBILE, To: +PLIVONUMBER, Text: YOURMESSAGEINFO: Sending XML Response to Plivo: <Response>...
- You should receive an SMS reply back on your mobile phone:
Thanks for your message! You said: ""YOURMESSAGE"" - Also, check the terminal running
ngrok. You should seePOST /api/plivoSmsWebhook 200 OKrequests logged.
5. Implementing Error Handling and Logging
Robust error handling and logging are essential for production systems. The following sections (6 and 7) will add database interaction and security validation, which should also be wrapped in error handling as shown here.
- RedwoodJS Logger: We are already using
loggerfromsrc/lib/logger. This provides basic logging capabilities. You can configure log levels (e.g.,trace,debug,info,warn,error,fatal) inapi/src/lib/logger.js. For production, you'll want to integrate with a dedicated logging service (like Logflare, Datadog, etc.).
- Webhook Error Handling Strategy:
- Parsing Errors: Catch errors during body parsing (as shown in the function). Return
400 Bad Request. - Missing Data: Check for required fields (
From,To,Text). Log a warning and return200 OKwith an empty<Response/>to prevent Plivo retries for malformed (but technically valid) requests. - Database Errors (Section 6): Wrap database operations (
db.smsMessage.create) in atry...catchblock. Log the error. Decide on the behavior (reply anyway, fail silently, reply with error). - Plivo SDK Errors: Wrap
response.addMessage()andresponse.toXML()intry...catch. Log the error and return200 OKwith empty<Response/>. - Signature Validation Errors (Section 7): If validation fails, log an error/warning and return
403 Forbidden.
- Parsing Errors: Catch errors during body parsing (as shown in the function). Return
-
Refined Webhook Code with Error Handling Structure: This version incorporates the error handling structure. The specific database and security logic will be added in Sections 6 and 7 where the
TODOcomments indicate.javascript// api/src/functions/plivoSmsWebhook.js (with enhanced error handling structure) import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Will be used in Section 6 import plivo from 'plivo' export const handler = async (event, context) => { logger.info('Received Plivo SMS webhook request') const emptyResponse = new plivo.Response() // For early exits // **TODO: Section 7 - Webhook Signature Validation will go here first** // Wrap signature validation in try/catch or check return value. // If invalid, return 403 immediately. // 1. Parse Body (assuming signature is valid or not yet implemented) let requestBody = {} if (typeof event.body === 'string') { try { requestBody = Object.fromEntries(new URLSearchParams(event.body)) } catch (e) { logger.error('Failed to parse request body:', e) return { statusCode: 400, body: 'Bad Request: Invalid body format' } } } else if (typeof event.body === 'object' && event.body !== null) { requestBody = event.body } else { logger.error('Received event with unexpected body type:', typeof event.body) return { statusCode: 400, body: 'Bad Request: Unexpected body format' } } // 2. Extract Data & Basic Validation const { From: fromNumber, To: toNumber, Text: text, MessageUUID: messageUuid } = requestBody if (!fromNumber || !toNumber || !text || !messageUuid) { logger.warn('Webhook received incomplete data:', requestBody) return { statusCode: 200, headers: { 'Content-Type': 'application/xml' }, body: emptyResponse.toXML() } } logger.info(`Processing message ${messageUuid} - From: ${fromNumber}, To: ${toNumber}, Text: ${text}`) // 3. Log to Database (with error handling) - Logic added in Section 6 try { // **TODO: Section 6 - Add DB save logic (incl. idempotency check) here** // Example: await db.smsMessage.create({ data: { ... } }); // logger.debug(`Message ${messageUuid} saved to database.`); } catch (dbError) { logger.error(`Failed to save message ${messageUuid} to DB:`, dbError); // Decide recovery strategy (e.g., continue processing? reply with error?) // For now, we log and continue to try replying. } // 4. Construct Reply (with error handling) let xmlResponse try { const response = new plivo.Response() // **TODO: Section 8 - Add STOP/HELP keyword handling logic here before the default reply** const replyText = `Thanks for your message! You said: ""${text}""` const params = { src: toNumber, dst: fromNumber } response.addMessage(replyText, params) xmlResponse = response.toXML() logger.info(`Generated XML response for ${messageUuid}:`, xmlResponse) } catch (xmlError) { logger.error(`Failed to generate XML response for ${messageUuid}:`, xmlError) // Failed to generate reply XML, return empty response to Plivo return { statusCode: 200, headers: { 'Content-Type': 'application/xml' }, body: emptyResponse.toXML() } } // 5. Send Response return { statusCode: 200, headers: { 'Content-Type': 'application/xml' }, body: xmlResponse, } } -
Retry Mechanisms & Idempotency: Plivo has its own webhook retry mechanism on
5xxerrors or timeouts. Returning200 OKprevents retries. To handle potential duplicate deliveries (e.g., due to network issues or retries before a200 OKwas received), make your webhook idempotent. This means processing the sameMessageUUIDmultiple times should not cause duplicate side effects. The database check shown in Section 6 helps achieve idempotency for message logging.
How Do You Store SMS Messages in a Database with Prisma?
Let's store the incoming messages in our database using Prisma.
-
Define Prisma Schema: Open
api/db/schema.prismaand add a model to store SMS messages:prisma// api/db/schema.prisma datasource db { provider = ""postgresql"" // Or your chosen provider url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } model SmsMessage { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt direction Direction // INBOUND or OUTBOUND fromNumber String toNumber String text String? // Can be null for non-text messages if needed plivoMessageUuid String? @unique // Plivo's ID, unique for lookups status String? // e.g., RECEIVED, SENT, FAILED, DELIVERED (requires status callbacks) rawPayload Json? // Store the raw Plivo webhook payload } enum Direction { INBOUND OUTBOUND }- Explanation:
id,createdAt,updatedAt: Standard audit fields.direction: Tracks if the message was incoming or outgoing using a Prisma Enum.fromNumber,toNumber,text: Core message details.plivoMessageUuid: Plivo's unique identifier. Making it@uniqueallows easy lookup and helps prevent duplicate processing (idempotency).status: Optional field to track delivery status (requires setting up Plivo status callbacks).rawPayload: Storing the raw JSON payload from Plivo can be useful for debugging.
- Explanation:
-
Create and Apply Migration: Run the Prisma migrate command to generate SQL and apply it to your database:
bashyarn rw prisma migrate dev --name add_sms_message_modelThis creates a new migration file in
api/db/migrations/and updates your database schema. -
Generate Prisma Client: RedwoodJS usually handles this automatically after migration, but you can run it manually if needed:
bashyarn rw prisma generate -
Update Webhook to Save Message: Modify
api/src/functions/plivoSmsWebhook.jswithin thetry...catchblock added in Section 5 to use thedbclient to save the incoming message. This includes an idempotency check usingplivoMessageUuid.javascript// api/src/functions/plivoSmsWebhook.js (add DB interaction in the designated TODO block) // ... imports ... import { db } from 'src/lib/db' // ... export const handler = async (event, context) => { // ... (parsing, validation logic) ... logger.info(`Processing message ${messageUuid} - From: ${fromNumber}, To: ${toNumber}, Text: ${text}`) // ... (Signature validation TODO - Section 7) ... // 3. Log to Database (with error handling & idempotency check) try { // Check if message already processed (idempotency) const existingMessage = await db.smsMessage.findUnique({ where: { plivoMessageUuid: messageUuid }, }); if (existingMessage) { logger.warn(`Message ${messageUuid} already processed. Skipping save.`); } else { await db.smsMessage.create({ data: { // Using the string literal 'INBOUND' which Prisma accepts for the Enum type. // Alternatively, for stricter type safety, especially in services, // you might import and use the enum directly: direction: Direction.INBOUND direction: 'INBOUND', fromNumber: fromNumber, toNumber: toNumber, text: text, plivoMessageUuid: messageUuid, status: 'RECEIVED', // Initial status rawPayload: requestBody, // Store the parsed payload }, }); logger.info(`Message ${messageUuid} saved to database.`); } } catch (dbError) { logger.error(`Failed to save or check message ${messageUuid} in DB:`, dbError); // Continue processing to attempt reply, even if DB save failed } // ... (Construct Reply, Send Response logic) ... } -
Data Access Patterns/Services (Optional but Recommended): For more complex applications, isolating database logic into RedwoodJS services is highly recommended for better organization, testability, and reusability. While this guide uses direct
dbcalls in the function for simplicity, here's how you might structure it using a service:- Generate the service:
bash
yarn rw g service smsMessages - Define functions in
api/src/services/smsMessages/smsMessages.js:javascript// api/src/services/smsMessages/smsMessages.js (Example) import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' import { Direction } from '@prisma/client' // Import the enum for type safety export const createSmsMessage = async ({ input }) => { logger.debug('Attempting to create SMS message:', input) // Add validation or transformation logic here if needed return db.smsMessage.create({ data: input }) } export const findSmsMessageByUuid = async ({ plivoMessageUuid }) => { return db.smsMessage.findUnique({ where: { plivoMessageUuid } }) } // Example usage in the webhook function (instead of direct db calls): // import { createSmsMessage, findSmsMessageByUuid } from 'src/services/smsMessages/smsMessages' // import { Direction } from '@prisma/client' // ... // const existingMessage = await findSmsMessageByUuid({ plivoMessageUuid: messageUuid }); // if (!existingMessage) { // await createSmsMessage({ input: { // direction: Direction.INBOUND, // Using Enum via service // fromNumber, toNumber, text, plivoMessageUuid, status: 'RECEIVED', rawPayload: requestBody // } }); // } - This service pattern is generally preferred for maintainable applications, but the direct
dbcalls remain functional for this basic example.
- Generate the service:
How Do You Secure Plivo Webhooks with Signature Validation?
Securing your webhook endpoint is critical to prevent unauthorized access and ensure data integrity. This section shows how to integrate signature validation into the handler function.
-
Webhook Signature Validation (Essential): Plivo signs webhook requests using your Auth Token. Verifying this signature is crucial.
- Enable Signatures in Plivo: In your Plivo Application configuration (Section 4, Step 3), provide your Auth ID and Auth Token. Plivo will then add
X-Plivo-Signature-V3,X-Plivo-Signature-V3-Nonce, andX-Plivo-Signature-V3-Timestampheaders to its requests. - Implement Validation: Use Plivo's
validateV3Signatureutility.
Integrate Validation into
plivoSmsWebhook.js: Add the following validation logic at the very beginning of thehandlerfunction, before parsing the body. - Enable Signatures in Plivo: In your Plivo Application configuration (Section 4, Step 3), provide your Auth ID and Auth Token. Plivo will then add
```javascript
// api/src/functions/plivoSmsWebhook.js (integrating signature validation)
import { logger } from 'src/lib/logger'
import { db } from 'src/lib/db'
import plivo from 'plivo'
export const handler = async (event, context) => {
logger.info('Received Plivo SMS webhook request')
const emptyResponse = new plivo.Response()
// --- 1. Webhook Signature Validation ---
// Plivo signature validation REQUIRES the raw, unparsed request body string.
// We assume event.body contains the raw string here. Verify this assumption
// based on your RedwoodJS version and any potential body-parsing middleware.
const rawBody = event.body;
if (typeof rawBody !== 'string') {
logger.error('Raw request body is not a string, cannot validate signature. Body type:', typeof rawBody);
return { statusCode: 400, body: 'Bad Request: Invalid body format for validation' };
}
const signature = event.headers['x-plivo-signature-v3']
const nonce = event.headers['x-plivo-signature-v3-nonce']
const timestamp = event.headers['x-plivo-signature-v3-timestamp']
const method = event.httpMethod // e.g., 'POST'
// CRITICAL: Construct the full callback URL *exactly* as Plivo sees it.
// This depends heavily on your deployment environment and proxy setup.
// Check headers like 'x-forwarded-proto', 'x-forwarded-host', 'host'.
// Log these headers during testing if validation fails.
const protocol = event.headers['x-forwarded-proto'] || (event.headers['host']?.includes('localhost') ? 'http' : 'https');
const host = event.headers['x-forwarded-host'] || event.headers['host'];
const path = event.path; // Ensure this includes the full path if needed
const fullUrl = `${protocol}://${host}${path}`; // Adjust if query params are involved
const authId = process.env.PLIVO_AUTH_ID
const authToken = process.env.PLIVO_AUTH_TOKEN
if (!authId || !authToken) {
logger.error('PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN not configured in environment variables.');
return { statusCode: 500, body: 'Server Configuration Error' };
}
if (!signature || !nonce || !timestamp) {
logger.warn('Missing signature headers. Request might not be from Plivo or signature validation not enabled in Plivo App.');
// Decide: Reject (403) or allow (if signature validation is optional)?
// For production, strongly recommend rejecting unsigned requests:
return { statusCode: 403, body: 'Forbidden: Missing signature headers' };
}
// Validate the signature
try {
const isValid = plivo.validateV3Signature(
method,
fullUrl,
nonce,
authToken, // Your Auth Token is used as the signing secret
signature,
rawBody
);
if (!isValid) {
logger.error(`Signature validation failed for request to ${fullUrl}. Possible causes: incorrect URL reconstruction, Auth Token mismatch, or request not from Plivo.`);
// Log headers for debugging (remove in production after validation works)
logger.debug('Request headers (for debugging URL reconstruction):', JSON.stringify(event.headers, null, 2));
return { statusCode: 403, body: 'Forbidden: Invalid signature' };
}
logger.info('Webhook signature validated successfully.');
} catch (validationError) {
logger.error('Error during signature validation:', validationError);
return { statusCode: 500, body: 'Internal Server Error during validation' };
}
// --- 2. Parse Body (now that signature is valid) ---
let requestBody = {}
// ... existing code ...
}
```
<!-- GAP: Missing replay attack prevention discussion (Type: Substantive) -->
<!-- GAP: Missing timestamp expiration check implementation (Type: Critical) -->
Frequently Asked Questions
What is the difference between Plivo XML and PHLO?
Plivo offers two approaches for handling webhooks: XML and PHLO (Plivo High-Level Objects). XML provides programmatic control where your code generates XML responses to instruct Plivo's actions, giving you full flexibility. PHLO uses a visual workflow builder in the Plivo console for simple use cases without code. This tutorial uses XML for maximum control and integration with your RedwoodJS application logic.
How do you handle concurrent webhook requests from Plivo?
RedwoodJS serverless functions handle concurrency automatically. Each webhook request runs in its own execution context. Use the MessageUUID as a unique identifier and implement idempotency checks in your database (as shown in the Prisma section) to prevent duplicate processing if Plivo retries a webhook due to network issues.
Can you send outbound SMS from RedwoodJS with Plivo?
Yes. Install the Plivo Node.js SDK, initialize a client with your Auth ID and Token, and call client.messages.create(). Store your credentials in environment variables and use the SDK within RedwoodJS services or functions. This tutorial focuses on inbound messages, but outbound messaging follows standard Plivo SDK patterns.
What happens if the webhook signature validation fails?
If signature validation fails, return HTTP 403 Forbidden to reject the request. Common causes include incorrect URL reconstruction (check proxy headers like x-forwarded-proto), Auth Token mismatch, or requests not originating from Plivo. Enable debug logging to inspect headers during troubleshooting, then remove debug logs in production.
How do you test Plivo webhooks locally without ngrok?
While ngrok is recommended for local testing, alternatives include: using Plivo's webhook simulator in the console (limited functionality), deploying to a staging environment with a public URL, or using other tunneling services like localtunnel or Cloudflare Tunnel. Ngrok remains the most reliable option for full end-to-end local testing.
Does RedwoodJS support WebSocket connections for real-time SMS?
RedwoodJS serverless functions don't support WebSockets natively. For real-time updates, implement polling from your frontend, use GraphQL subscriptions with a separate WebSocket server, or integrate with real-time services like Pusher or Supabase Realtime. The webhook-to-database pattern shown here works well with polling-based real-time UIs.
How do you handle international SMS with E.164 formatting?
Plivo sends and expects phone numbers in E.164 format (+[country code][number]). Store numbers in E.164 format in your database. Use libraries like libphonenumber-js for validation and formatting. The From and To parameters in Plivo webhooks are already E.164 formatted, so no additional parsing is needed in most cases.
What are the rate limits for Plivo webhooks?
Plivo doesn't impose strict rate limits on incoming webhooks – they send webhooks as SMS messages arrive. However, your RedwoodJS deployment infrastructure may have limits. Ensure your database and serverless function can handle your expected message volume. Monitor function execution times and optimize database queries for high-traffic scenarios.
<!-- GAP: Missing specific serverless platform limits (Vercel/Netlify) (Type: Substantive) -->How do you implement STOP/START/HELP keyword handling?
Add keyword detection logic in your webhook handler before generating the reply. Check if text.toUpperCase() matches keywords like "STOP", "START", or "HELP". For STOP, update a subscription status in your database and return an empty <Response/> to prevent further messages. For HELP, return information about your service. This is required for compliance with SMS regulations in many countries.
Can you use Plivo with RedwoodJS on Vercel or Netlify?
Yes. RedwoodJS deploys to both Vercel and Netlify with serverless functions. The webhook handler works identically on both platforms. Ensure your DATABASE_URL and Plivo environment variables are configured in your deployment platform's environment settings. Pay attention to URL construction in signature validation – use x-forwarded-* headers correctly for each platform.
Frequently Asked Questions
How to set up two-way SMS in RedwoodJS?
Start by creating a new RedwoodJS project, installing the Plivo Node.js SDK, and setting up environment variables for your Plivo Auth ID, Auth Token, and Plivo phone number. You'll then create a RedwoodJS API function to handle incoming webhooks from Plivo.
What is Plivo used for in RedwoodJS SMS integration?
Plivo is a cloud communications platform that provides the SMS API for sending and receiving messages. It handles the actual SMS delivery and interacts with your RedwoodJS application via webhooks.
Why use RedwoodJS for a two-way SMS application?
RedwoodJS offers a full-stack, serverless framework with built-in tools for APIs, databases, and web frontends. This simplifies development and deployment of complex SMS applications.
When should I validate the Plivo webhook signature?
Signature validation is crucial for security and should be performed at the very beginning of your webhook handler function, before processing any data from the request. This prevents unauthorized access to your application.
Can I test Plivo SMS integration locally?
Yes, you can use ngrok to create a public URL that tunnels requests to your local development server. Configure your Plivo application to send webhooks to this ngrok URL, allowing you to test the integration locally before deploying.
How to handle Plivo webhook errors in RedwoodJS?
Implement robust error handling using try-catch blocks around body parsing, database operations, and Plivo SDK calls. Log errors using Redwood's logger and return a 200 OK response to Plivo, even if an error occurs, to prevent retries. For signature validation failures, return a 403 Forbidden response.
What is the purpose of the Plivo MessageUUID?
The MessageUUID is a unique identifier assigned by Plivo to each incoming SMS message. Use this UUID to check for duplicate webhook deliveries and ensure idempotent processing, preventing duplicate database entries or other side effects.
How to send an SMS reply with Plivo in RedwoodJS?
Use the Plivo Node.js SDK to construct an XML response containing a `<Message>` element with the `src` (your Plivo number) and `dst` (recipient's number). The text of the reply is set within the `<Message>` element. Return this XML in the 200 OK response to the Plivo webhook.
What database is recommended for RedwoodJS Plivo integration?
While Prisma supports various databases, PostgreSQL is generally recommended for production use due to its reliability and features. SQLite can be used for development or simpler projects.
How to create a database schema for SMS messages in RedwoodJS?
Define a Prisma schema in `api/db/schema.prisma` with fields like `fromNumber`, `toNumber`, `text`, `plivoMessageUuid`, `status`, and a `Direction` enum. Then run `yarn rw prisma migrate dev` to apply the schema to your database and generate the Prisma Client.
What does the RedwoodJS Plivo webhook endpoint receive?
The webhook receives an HTTP POST request with form-urlencoded data containing parameters like `From`, `To`, `Text`, `MessageUUID`, `Type`, and `Event` from Plivo. The request also includes signature headers if enabled in your Plivo application.
Why is an XML response needed for Plivo webhooks?
Plivo uses XML (Plivo XML or PHLO) to control communication flows. The XML response you return from your webhook instructs Plivo on the next action, such as sending an SMS reply, playing audio, or gathering user input.
How to parse Plivo webhook data in RedwoodJS?
Plivo sends webhook data as application/x-www-form-urlencoded. Use `URLSearchParams` or similar methods in your RedwoodJS function to parse the request body string into a JavaScript object. Be sure to handle potential errors during this process and check for all essential parameters.