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.
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.
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_url
you 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).
ngrok
installed for local development testing (Download ngrok).- Basic understanding of RedwoodJS concepts (API functions, services, Prisma).
1. Setting Up the Project
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
-
Create RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-plivo-sms cd redwood-plivo-sms
Choose your preferred database during setup (PostgreSQL is recommended for production). For this guide, we'll assume PostgreSQL.
-
Install Plivo SDK: The Plivo SDK is needed on the API side to interact with Plivo APIs and generate response XML.
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
.env
file in the root of your RedwoodJS project (if it doesn't exist). - Add your Plivo credentials and your Plivo phone number to the
.env
file using the standardKEY=VALUE
format:
# .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_NUMBER
is stored here for easy reference when constructing replies or sending outbound messages.
-
Git Initialization: It's good practice to initialize Git early.
git init git add . git commit -m ""Initial project setup with RedwoodJS and Plivo SDK""
Make sure
.env
is included in your.gitignore
file (RedwoodJS adds it by default).
2. Implementing Core Functionality (Webhook Handler)
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:
yarn rw g function plivoSmsWebhook
This creates
api/src/functions/plivoSmsWebhook.js
. -
Implement Webhook Logic: Open
api/src/functions/plivoSmsWebhook.js
and replace its contents with the following code:// 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), anddst
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 toapplication/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).
- We import Redwood's logger and Prisma client (
- Explanation:
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 allowsGET
too – 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
:sms
MessageUUID
: 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.:
<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 OK
with 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
ngrok
is 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.# 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.
4. Integrating with Plivo (Configuration)
Now, let's configure Plivo to send webhooks to our local development server and then to our deployed application.
-
Start Local Development Server:
yarn rw dev
Your RedwoodJS app (including the API function) is now running, typically accessible via
http://localhost:8910
for the web side andhttp://localhost:8911
for 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:
ngrok http 8911
- Note: Port
8911
is the default for RedwoodJS API functions. Verify this in yourredwood.toml
or dev server output if needed. ngrok
will display a Forwarding URL (e.g.,https://abcdef123456.ngrok.io
). This URL tunnels requests to yourlocalhost:8911
. Copy thehttps
version 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
ngrok
forwarding 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 request
INFO: Message received - From: +YOURMOBILE, To: +PLIVONUMBER, Text: YOURMESSAGE
INFO: 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 OK
requests 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
logger
fromsrc/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 OK
with 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...catch
block. 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 OK
with 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
TODO
comments indicate.// 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
5xx
errors or timeouts. Returning200 OK
prevents retries. To handle potential duplicate deliveries (e.g., due to network issues or retries before a200 OK
was received), make your webhook idempotent. This means processing the sameMessageUUID
multiple times should not cause duplicate side effects. The database check shown in Section 6 helps achieve idempotency for message logging.
6. Creating a Database Schema and Data Layer
Let's store the incoming messages in our database using Prisma.
-
Define Prisma Schema: Open
api/db/schema.prisma
and add a model to store SMS messages:// 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@unique
allows 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:
yarn rw prisma migrate dev --name add_sms_message_model
This 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:
yarn rw prisma generate
-
Update Webhook to Save Message: Modify
api/src/functions/plivoSmsWebhook.js
within thetry...catch
block added in Section 5 to use thedb
client to save the incoming message. This includes an idempotency check usingplivoMessageUuid
.// 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
db
calls in the function for simplicity, here's how you might structure it using a service:- Generate the service:
yarn rw g service smsMessages
- Define functions in
api/src/services/smsMessages/smsMessages.js
:// 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
db
calls remain functional for this basic example.
- Generate the service:
7. Adding Security Features
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-Timestamp
headers to its requests. - Implement Validation: Use Plivo's
validateV3Signature
utility.
Integrate Validation into
plivoSmsWebhook.js
: Add the following validation logic at the very beginning of thehandler
function, before parsing the body.// 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 au
- Enable Signatures in Plivo: In your Plivo Application configuration (Section 4, Step 3), provide your Auth ID and Auth Token. Plivo will then add