code examples

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

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.

<!-- DEPTH: Introduction lacks estimated completion time and prerequisite skill level (Priority: Medium) -->

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:

  1. Receive incoming SMS messages sent to a Plivo phone number.
  2. Process the message content (e.g., log it).
  3. 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.
<!-- GAP: Missing minimum version requirements for technologies (Type: Critical) -->

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 '------------------------------------------------------------------------'
  1. A user sends an SMS to your Plivo phone number.
  2. Plivo receives the SMS and sends an HTTP POST request (webhook) to the message_url you configure.
  3. Your RedwoodJS API function receives this webhook request.
  4. The function parses the incoming data (From, To, Text).
  5. (Optional but recommended) The function validates the webhook signature to ensure it came from Plivo.
  6. The function logs the incoming message details to your database using Prisma.
  7. The function constructs an XML response using the Plivo Node.js SDK, instructing Plivo to send a reply SMS.
  8. The function sends this XML back to Plivo with an HTTP 200 OK status.
  9. 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).
<!-- GAP: Missing RedwoodJS version compatibility information (Type: Critical) --> <!-- DEPTH: Prerequisites don't specify knowledge level required (Priority: Medium) -->

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.

  1. Create RedwoodJS App: Open your terminal and run:
    bash
    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.
<!-- DEPTH: Missing explanation of what happens during project creation (Priority: Low) -->
  1. 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
<!-- GAP: Missing Plivo SDK version specification (Type: Substantive) -->
  1. 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 standard KEY=VALUE format:
    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_NUMBER is stored here for easy reference when constructing replies or sending outbound messages.
<!-- GAP: Missing security warning about .env file permissions (Type: Critical) -->
  1. Git Initialization: It's good practice to initialize Git early.
    bash
    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).

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.

  1. Generate API Function: Use the RedwoodJS CLI to generate a new serverless function:

    bash
    yarn rw g function plivoSmsWebhook

    This creates api/src/functions/plivoSmsWebhook.js.

  2. Implement Webhook Logic: Open api/src/functions/plivoSmsWebhook.js and replace its contents with the following code:

<!-- DEPTH: Code block lacks explanation of key design decisions (Priority: High) --> ```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 on fromNumber) 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 allows GET 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 message
      • Event: 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>
    • 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, return 403 Forbidden.
<!-- DEPTH: Missing comprehensive parameter table with descriptions (Priority: High) -->
  • 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.

    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.

<!-- EXPAND: Could benefit from Postman collection example (Type: Enhancement) -->

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.

  1. Start Local Development Server:

    bash
    yarn rw dev

    Your RedwoodJS app (including the API function) is now running, typically accessible via http://localhost:8910 for the web side and http://localhost:8911 for the API/functions (check your terminal output). Our webhook is at http://localhost:8911/plivoSmsWebhook.

  2. Expose Local Server with ngrok: Plivo needs a publicly accessible URL to send webhooks. Open a new terminal window and run:

    bash
    ngrok http 8911
    • Note: Port 8911 is the default for RedwoodJS API functions. Verify this in your redwood.toml or dev server output if needed.
    • ngrok will display a Forwarding URL (e.g., https://abcdef123456.ngrok.io). This URL tunnels requests to your localhost:8911. Copy the https version of this URL.
<!-- GAP: Missing ngrok alternatives and comparison (Type: Substantive) -->
  1. 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.
<!-- DEPTH: Missing screenshot or visual reference for Plivo console (Priority: Low) -->
  1. 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.
  2. 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 see POST /api/plivoSmsWebhook 200 OK requests logged.
<!-- GAP: Missing troubleshooting steps for common setup failures (Type: Substantive) -->

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.

  1. RedwoodJS Logger: We are already using logger from src/lib/logger. This provides basic logging capabilities. You can configure log levels (e.g., trace, debug, info, warn, error, fatal) in api/src/lib/logger.js. For production, you'll want to integrate with a dedicated logging service (like Logflare, Datadog, etc.).
<!-- EXPAND: Could benefit from specific logging service integration examples (Type: Enhancement) -->
  1. 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 return 200 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 a try...catch block. Log the error. Decide on the behavior (reply anyway, fail silently, reply with error).
    • Plivo SDK Errors: Wrap response.addMessage() and response.toXML() in try...catch. Log the error and return 200 OK with empty <Response/>.
    • Signature Validation Errors (Section 7): If validation fails, log an error/warning and return 403 Forbidden.
<!-- DEPTH: Missing alert/monitoring strategy discussion (Priority: Medium) -->
  1. 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.

    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,
      }
    }
  2. Retry Mechanisms & Idempotency: Plivo has its own webhook retry mechanism on 5xx errors or timeouts. Returning 200 OK prevents retries. To handle potential duplicate deliveries (e.g., due to network issues or retries before a 200 OK was received), make your webhook idempotent. This means processing the same MessageUUID multiple times should not cause duplicate side effects. The database check shown in Section 6 helps achieve idempotency for message logging.

<!-- GAP: Missing retry timing and maximum attempts from Plivo (Type: Substantive) -->

How Do You Store SMS Messages in a Database with Prisma?

Let's store the incoming messages in our database using Prisma.

  1. Define Prisma Schema: Open api/db/schema.prisma and 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 @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.
<!-- GAP: Missing index recommendations for query performance (Type: Substantive) --> <!-- EXPAND: Could benefit from data retention policy guidance (Type: Enhancement) -->
  1. Create and Apply Migration: Run the Prisma migrate command to generate SQL and apply it to your database:

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

  2. Generate Prisma Client: RedwoodJS usually handles this automatically after migration, but you can run it manually if needed:

    bash
    yarn rw prisma generate
  3. Update Webhook to Save Message: Modify api/src/functions/plivoSmsWebhook.js within the try...catch block added in Section 5 to use the db client to save the incoming message. This includes an idempotency check using plivoMessageUuid.

    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) ...
    }
  4. 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:
      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 db calls remain functional for this basic example.
<!-- EXPAND: Could benefit from testing examples for services (Type: Enhancement) -->

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.

  1. 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, and X-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 the handler function, before parsing the body.

<!-- DEPTH: Missing explanation of signature algorithm and security implications (Priority: Medium) --> ```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.

<!-- DEPTH: Missing concurrency limits and scaling considerations (Priority: Medium) -->

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.

<!-- EXPAND: Could benefit from complete outbound SMS code example (Type: Enhancement) -->

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.

<!-- EXPAND: Could benefit from E.164 validation code example (Type: Enhancement) -->

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.

<!-- GAP: Missing complete STOP/START/HELP implementation example (Type: Critical) --> <!-- GAP: Missing TCPA and SMS compliance requirements (Type: Critical) -->

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.

<!-- GAP: Missing deployment walkthrough for production (Type: Substantive) --> <!-- GAP: Missing monitoring and observability setup (Type: Substantive) --> <!-- EXPAND: Could benefit from CI/CD pipeline example (Type: Enhancement) -->

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.