code examples

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

Developer Guide: Implementing Plivo SMS Delivery Status Callbacks in RedwoodJS

A step-by-step guide for integrating Plivo SMS sending and delivery status callbacks into a RedwoodJS application using webhooks and Prisma.

This guide provides a step-by-step walkthrough for integrating Plivo SMS services into your RedwoodJS application, focusing specifically on sending messages and reliably tracking their delivery status using Plivo's callback mechanism. We'll build a system that sends an SMS, stores its initial details, and updates its status in real-time as Plivo provides delivery reports via webhooks.

By the end of this tutorial, you will have:

  • A RedwoodJS application capable of sending SMS messages via the Plivo API.
  • A database schema to store message details and track delivery status.
  • A secure webhook endpoint (RedwoodJS Function) to receive status updates from Plivo.
  • Implementation of Plivo's request signature validation for webhook security.
  • Instructions for local development testing using ngrok.
  • Guidance on deployment considerations.

Technologies Used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. We leverage its API-side (GraphQL, Services, Functions) and database integration (Prisma).
  • Plivo: A cloud communications platform providing SMS APIs.
  • Prisma: A next-generation ORM used by RedwoodJS for database access.
  • Node.js: The underlying runtime environment.
  • ngrok (for development): A tool to expose local development servers to the internet.

System Architecture:

mermaid
sequenceDiagram
    participant Client as Client (e.g., Web Browser)
    participant RedwoodWeb as RedwoodJS Web Side
    participant RedwoodAPI as RedwoodJS API Side (GraphQL)
    participant MessagesService as Messages Service
    participant PlivoAPI as Plivo API
    participant Database as Database (Prisma)
    participant PlivoCallbackFn as RedwoodJS Plivo Callback Function
    participant PlivoPlatform as Plivo Platform

    Client->>RedwoodWeb: Trigger Send SMS Action
    RedwoodWeb->>RedwoodAPI: GraphQL Mutation (sendMessage)
    RedwoodAPI->>MessagesService: Call sendMessage(to, body)
    MessagesService->>PlivoAPI: Send SMS Request (POST /Message/) [Includes Callback URL]
    PlivoAPI-->>MessagesService: Acknowledge (message_uuid)
    MessagesService->>Database: Store Message (plivoMessageId, to, body, status='queued')
    MessagesService-->>RedwoodAPI: Return Success/Message ID
    RedwoodAPI-->>RedwoodWeb: Response
    RedwoodWeb-->>Client: Update UI

    Note over PlivoPlatform: Message processing occurs...

    PlivoPlatform->>PlivoCallbackFn: POST Request to Callback URL [MessageUUID, Status]
    Note over PlivoCallbackFn: Internally validates Plivo Signature
    alt Signature Valid
        PlivoCallbackFn->>Database: Find Message by plivoMessageId (MessageUUID)
        PlivoCallbackFn->>Database: Update Message Status
        PlivoCallbackFn-->>PlivoPlatform: HTTP 200 OK
    else Signature Invalid
        PlivoCallbackFn-->>PlivoPlatform: HTTP 401 Unauthorized / 403 Forbidden
    end

Prerequisites:

  • Node.js and Yarn installed.
  • A Plivo account with Auth ID, Auth Token, and a Plivo phone number capable of sending SMS.
  • Basic understanding of RedwoodJS concepts (Workspaces, Services, Functions, Prisma).
  • ngrok installed for local development testing.
  • A database supported by Prisma (e.g., PostgreSQL, SQLite).

1. Setting up the RedwoodJS Project

First, create a new RedwoodJS project if you don't have one already. We'll use TypeScript for this guide.

bash
# Create a new RedwoodJS app (choose TypeScript when prompted)
yarn create redwood-app ./redwood-plivo-callbacks
cd redwood-plivo-callbacks

# Install the Plivo Node.js helper library in the API workspace
yarn workspace api add plivo

Environment Variables:

Plivo requires authentication credentials and a source phone number. We also need a base URL for our callback endpoint, especially during development with ngrok.

Create a .env file in the root of your project and add the following variables. You can find your Plivo Auth ID and Auth Token in the Plivo Console dashboard under "API" -> "Keys & Credentials". Your Plivo phone number is listed under "Phone Numbers".

dotenv
# .env

# Plivo Credentials (Get from Plivo Console: https://console.plivo.com/dashboard/)
PLIVO_AUTH_ID="YOUR_PLIVO_AUTH_ID"
PLIVO_AUTH_TOKEN="YOUR_PLIVO_AUTH_TOKEN"
PLIVO_SOURCE_NUMBER="YOUR_PLIVO_PHONE_NUMBER" # E.g., +14155551212

# Base URL for the Plivo callback webhook
# For local dev using ngrok, it will be like https://xxxx-xxxx.ngrok.io
# For production, it will be your deployed API domain (MUST use HTTPS)
# Update this immediately after starting ngrok or deploying.
PLIVO_CALLBACK_BASE_URL="http://localhost:8911" # Default for local dev before ngrok
  • PLIVO_AUTH_ID / PLIVO_AUTH_TOKEN: Your API credentials for authenticating requests to Plivo. Find these on the Plivo Console.
  • PLIVO_SOURCE_NUMBER: The Plivo phone number you will send messages from.
  • PLIVO_CALLBACK_BASE_URL: The public base URL where your API is accessible. Plivo will POST status updates to a path under this URL (e.g., ${PLIVO_CALLBACK_BASE_URL}/plivoCallback). This must be updated to use https:// for ngrok or production.

RedwoodJS Configuration:

Ensure your redwood.toml includes the API environment variables:

toml
# redwood.toml

[web]
  title = "Redwood App"
  port = 8910
  apiUrl = "/.redwood/functions" # Default Redwood setting
  includeEnvironmentVariables = []
[api]
  port = 8911
  host = "localhost"
  # Make Plivo env vars available to the API side
  includeEnvironmentVariables = ["PLIVO_AUTH_ID", "PLIVO_AUTH_TOKEN", "PLIVO_SOURCE_NUMBER", "PLIVO_CALLBACK_BASE_URL"]
[browser]
  open = true

2. Creating the Database Schema and Data Layer

We need a database table to store information about the messages we send, including their Plivo ID and delivery status.

Define the Prisma Schema:

Open api/db/schema.prisma and add a Message model:

prisma
// api/db/schema.prisma

datasource db {
  provider = ""sqlite"" // Or ""postgresql"", ""mysql"", etc.
  url      = env(""DATABASE_URL"")
}

generator client {
  provider      = ""prisma-client-js""
  binaryTargets = ""native""
}

// Define your Message model
model Message {
  id             Int      @id @default(autoincrement())
  plivoMessageId String   @unique // The UUID returned by Plivo when sending
  to             String   // Recipient phone number
  body           String   // Message content
  status         String   // e.g., 'queued', 'sent', 'delivered', 'failed', 'undelivered'
  plivoErrorCode Int?     // Plivo error code if status is 'failed' or 'undelivered'
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
}
  • plivoMessageId: Stores the unique identifier (message_uuid) returned by Plivo upon successful submission. This is crucial for correlating callbacks.
  • status: Tracks the delivery status reported by Plivo.
  • plivoErrorCode: Stores the Plivo error code if the message fails.

Apply Migrations:

Generate and apply the database migration:

bash
# Generate SQL migration files and apply to the database
yarn rw prisma migrate dev --name add_message_model

This command creates a migration file in api/db/migrations and updates your database schema.


3. Implementing Core Functionality: Sending SMS

We'll create a RedwoodJS Service and a GraphQL mutation to handle sending SMS messages via Plivo.

Create the Messages Service:

Generate the service and SDL files for messages:

bash
yarn rw g service messages

Implement the sendMessage Service Function:

Edit api/src/services/messages/messages.ts to include the logic for sending SMS via Plivo and saving the initial record to the database.

typescript
// api/src/services/messages/messages.ts

import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import { validate } from '@redwoodjs/api'
import Plivo from 'plivo' // Import the Plivo SDK

// Initialize Plivo client (ensure env vars are loaded)
const plivoClient = new Plivo.Client(
  process.env.PLIVO_AUTH_ID,
  process.env.PLIVO_AUTH_TOKEN
)

interface SendMessageInput {
  to: string
  body: string
}

export const sendMessage = async ({ input }: { input: SendMessageInput }) => {
  validate(input.to, 'Recipient Number', { presence: true })
  // Add more specific phone number validation if needed
  validate(input.body, 'Message Body', { presence: true, length: { max: 1600 } })

  const { to, body } = input
  const sourceNumber = process.env.PLIVO_SOURCE_NUMBER
  const callbackBaseUrl = process.env.PLIVO_CALLBACK_BASE_URL

  if (!sourceNumber) {
    logger.error('PLIVO_SOURCE_NUMBER environment variable is not set.')
    throw new Error('SMS configuration error: Missing source number.')
  }
  if (!callbackBaseUrl) {
    logger.error('PLIVO_CALLBACK_BASE_URL environment variable is not set.')
    throw new Error('SMS configuration error: Missing callback base URL.')
  }
  if (!callbackBaseUrl.startsWith('https://') && !callbackBaseUrl.includes('localhost')) {
     logger.warn('PLIVO_CALLBACK_BASE_URL does not start with https://. Plivo requires HTTPS for callbacks in production.')
     // Consider throwing an error in production environments if not HTTPS
  }


  // Construct the full callback URL
  // Plivo will POST status updates to this endpoint
  const callbackUrl = `${callbackBaseUrl}/plivoCallback` // Matches our function name

  try {
    logger.info(`Attempting to send SMS via Plivo to ${to}`)

    const response = await plivoClient.messages.create(
      sourceNumber, // src
      to,           // dst
      body,         // text
      {
        // CRUCIAL: Provide the URL for delivery status callbacks
        url: callbackUrl,
        method: 'POST', // Plivo defaults to POST
      }
    )

    logger.info({ plivoResponse: response }, 'Plivo SMS send response')

    // Plivo returns an array of message UUIDs, even for single messages
    if (response.messageUuid && response.messageUuid.length > 0) {
      const plivoMessageId = response.messageUuid[0]

      // Save the initial message state to the database
      const newMessage = await db.message.create({
        data: {
          plivoMessageId: plivoMessageId,
          to: to,
          body: body,
          status: 'queued', // Initial status upon successful submission to Plivo
        },
      })

      logger.info(
        { messageId: newMessage.id, plivoMessageId },
        'Message saved to database with status queued'
      )
      return newMessage // Return the created message record
    } else {
      // Handle cases where Plivo API might succeed but not return a UUID (unlikely)
      logger.error({ plivoResponse: response }, 'Plivo response missing message_uuid')
      throw new Error('Failed to retrieve message ID from Plivo.')
    }
  } catch (error) {
    logger.error({ error, to, sourceNumber }, 'Error sending SMS via Plivo')
    // Consider more specific error handling based on Plivo error codes/types
    throw new Error(`Failed to send SMS: ${error.message || 'Unknown Plivo error'}`)
  }
}

// Optional: Add service functions to retrieve messages if needed
export const messages = () => {
  return db.message.findMany({ orderBy: { createdAt: 'desc' } })
}

export const message = ({ id }: { id: number }) => {
  return db.message.findUnique({ where: { id } })
}
  • We initialize the Plivo client using environment variables.
  • The sendMessage function takes to and body as input.
  • Crucially, we construct the callbackUrl using PLIVO_CALLBACK_BASE_URL and append /plivoCallback (this must match the filename of our webhook function later). This URL is passed in the url parameter of the plivoClient.messages.create call.
  • We handle the response from Plivo, extracting the message_uuid.
  • We create a record in our Message table with the plivoMessageId and an initial status of 'queued'.
  • Basic error handling and logging are included.

Define the GraphQL Schema (SDL):

Update api/src/graphql/messages.sdl.ts to define the mutation and types.

typescript
// api/src/graphql/messages.sdl.ts

export const schema = gql`
  type Message {
    id: Int!
    plivoMessageId: String!
    to: String!
    body: String!
    status: String!
    plivoErrorCode: Int
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Query {
    messages: [Message!]! @requireAuth
    message(id: Int!): Message @requireAuth
  }

  input SendMessageInput {
    to: String!
    body: String!
  }

  type Mutation {
    sendMessage(input: SendMessageInput!): Message! @requireAuth
    # Add requireAuth or skipAuth based on your app's security needs
  }
`

This defines the Message type mirroring our Prisma model, a query to fetch messages, and the sendMessage mutation which accepts the SendMessageInput and returns the created Message. We've added @requireAuth as a placeholder; adjust authentication as needed for your application.


4. Building the Plivo Callback API Endpoint

Plivo will send HTTP POST requests to the url we provided when sending the message. We need a RedwoodJS Function to receive and process these requests.

Create the Plivo Callback Function:

bash
yarn rw g function plivoCallback --typescript

This creates api/src/functions/plivoCallback.ts.

Implement the Callback Handler:

Edit api/src/functions/plivoCallback.ts to handle incoming Plivo webhooks, validate signatures, and update the database.

typescript
// api/src/functions/plivoCallback.ts

import type { APIGatewayEvent, Context } from 'aws-lambda'
import { logger } from 'src/lib/logger'
import { db } from 'src/lib/db'
import Plivo from 'plivo' // Import Plivo to use the signature validation utility
import { URLSearchParams } from 'url' // Node.js built-in for parsing form data

/**
 * Handles incoming Plivo Message Status Callbacks.
 * Verifies the request signature and updates the message status in the database.
 * See: https://www.plivo.com/docs/messaging/api/message/message-status-callbacks/
 * And: https://www.plivo.com/docs/getting-started/security-best-practices/#validate-plivo-signatures
 */
export const handler = async (event: APIGatewayEvent, _context: Context) => {
  logger.info({ headers: event.headers, body: event.body }, 'Received Plivo webhook')

  const plivoSignature = event.headers['X-Plivo-Signature-V3']
  const nonce = event.headers['X-Plivo-Signature-V3-Nonce']
  const requestUrl = `${process.env.PLIVO_CALLBACK_BASE_URL}${event.path}`
  // Note: Assumes event.path includes the leading '/', common in AWS Lambda/API Gateway. Verify for your specific deployment platform.

  // --- Security: Validate Plivo Signature ---
  // This is ESSENTIAL to ensure the request genuinely comes from Plivo and prevent spoofing.
  const authToken = process.env.PLIVO_AUTH_TOKEN
  if (!authToken) {
    logger.error('PLIVO_AUTH_TOKEN is not configured. Cannot validate signature.')
    return { statusCode: 500, body: 'Internal Server Error: Configuration missing.' }
  }

  if (!plivoSignature || !nonce) {
    logger.warn('Missing Plivo signature or nonce headers')
    return { statusCode: 400, body: 'Bad Request: Missing signature headers.' }
  }

  try {
    // Use Plivo's utility function to validate the signature
    // IMPORTANT: Pass the raw event body string for validation
    const isValid = Plivo.validateV3Signature(
      event.httpMethod, // Should be 'POST'
      requestUrl,
      nonce,
      authToken,
      plivoSignature,
      event.body // Pass the raw body string
    )

    if (!isValid) {
      logger.error('Invalid Plivo signature received.')
      return { statusCode: 403, body: 'Forbidden: Invalid signature.' }
    }
    logger.info('Plivo signature validated successfully.')

  } catch (error) {
     logger.error({ error }, 'Error during signature validation')
     // Log the specific error, but return a generic server error
     return { statusCode: 500, body: 'Internal Server Error during validation.' }
  }
  // --- End Security Check ---


  // --- Process the Callback Data ---
  let payload: Record<string, any>
  try {
    // Plivo typically sends data as application/x-www-form-urlencoded or application/json.
    // Check the Content-Type header to determine how to parse.
    // RedwoodJS/API Gateway might auto-parse JSON if the Content-Type is correct.
    const contentType = event.headers['Content-Type'] || event.headers['content-type'] || '';
    logger.info(`Processing callback with Content-Type: ${contentType}`)

    if (typeof event.body === 'string') {
       if (contentType.includes('application/json')) {
         payload = JSON.parse(event.body);
         logger.info('Parsed JSON payload');
       } else if (contentType.includes('application/x-www-form-urlencoded')) {
         // Use URLSearchParams for robust form data parsing
         payload = Object.fromEntries(new URLSearchParams(event.body));
         logger.info({ parsedPayload: payload }, 'Parsed form-urlencoded data');
       } else {
         // Fallback attempt: Try JSON parse if Content-Type is missing or unexpected.
         // Consult Plivo docs if you encounter other content types.
         logger.warn({ contentType }, 'Unexpected or missing Content-Type, attempting JSON parse');
         payload = JSON.parse(event.body);
       }
    } else if (event.body && typeof event.body === 'object'){
       // Body might be pre-parsed by the environment (e.g., Lambda Proxy integration)
       payload = event.body as Record<string, any>;
       logger.info('Using pre-parsed payload object');
    } else {
        logger.error('Request body is missing or in an unexpected format.')
        // Cannot proceed without a body
        return { statusCode: 400, body: 'Bad Request: Missing or invalid payload body.' }
    }


    const plivoMessageId = payload.MessageUUID // Plivo uses 'MessageUUID'
    const status = payload.MessageStatus       // Plivo uses 'MessageStatus'
    const errorCode = payload.ErrorCode ? parseInt(payload.ErrorCode, 10) : null

    if (!plivoMessageId || !status) {
      logger.warn({ payload }, 'Received Plivo callback missing MessageUUID or MessageStatus')
      // Return 200 OK to Plivo to prevent retries for malformed data we can't process.
      return { statusCode: 200, body: 'OK (Missing required fields)' }
    }

    logger.info(
      { plivoMessageId, status, errorCode },
      'Processing Plivo status update'
    )

    // Find the message in the database and update its status
    const updatedMessage = await db.message.update({
      where: { plivoMessageId: plivoMessageId },
      data: {
        status: status, // e.g., 'sent', 'delivered', 'failed', 'undelivered'
        plivoErrorCode: errorCode,
        updatedAt: new Date(), // Ensure updatedAt is refreshed
      },
    })

    if (updatedMessage) {
      logger.info(
        { messageId: updatedMessage.id, plivoMessageId, newStatus: status },
        'Successfully updated message status in database'
      )
    } else {
      // This might happen if the callback arrives before the sendMessage service finished writing the initial record,
      // or if the MessageUUID is incorrect. Plivo might retry.
      logger.warn(
        { plivoMessageId },
        'Could not find matching message in database for Plivo callback'
      )
      // Still return 200 OK to Plivo for valid requests, even if we didn't find the message yet.
      // Avoid causing Plivo to retry indefinitely for potentially temporary timing issues.
      return { statusCode: 200, body: 'OK (Message not found, might be processed later)' }
    }

  } catch (error) {
    logger.error({ error, body: event.body }, 'Error processing Plivo callback payload')
    // Return 500 to signal Plivo that processing failed and it might need to retry (if configured).
    return { statusCode: 500, body: 'Internal Server Error processing callback.' }
  }

  // Acknowledge receipt to Plivo
  return {
    statusCode: 200,
    body: 'OK', // Plivo expects a 200 OK
  }
}
  • Logging: We log the incoming headers and body for debugging.
  • Signature Validation:
    • We retrieve the X-Plivo-Signature-V3 and X-Plivo-Signature-V3-Nonce headers.
    • We reconstruct the full requestUrl that Plivo used when sending the webhook.
    • We use Plivo.validateV3Signature with the request method (POST), URL, nonce, your Auth Token, the received signature, and the raw request body string.
    • This validation is critical for security. If it fails, we return a 403 Forbidden response.
  • Payload Processing:
    • The code now explicitly checks the Content-Type header to decide between parsing JSON or x-www-form-urlencoded data (using URLSearchParams). It also handles cases where the body might already be parsed by the environment.
    • It extracts MessageUUID, MessageStatus, and ErrorCode from the payload.
  • Database Update:
    • We use db.message.update with a where clause targeting the plivoMessageId (which corresponds to Plivo's MessageUUID).
    • We update the status and plivoErrorCode.
  • Response: We return a 200 OK to Plivo to acknowledge successful receipt and processing. If signature validation fails, return 403. If payload processing fails, return 500 to potentially trigger Plivo retries (check Plivo's retry policy). Return 200 even if the message isn't found immediately to avoid unnecessary retries for timing issues.

5. Local Development and Testing with ngrok

To receive Plivo webhooks on your local machine, you need to expose your local Redwood API server to the public internet. ngrok is perfect for this.

Steps:

  1. Start your Redwood development server:

    bash
    yarn rw dev

    Your API server will typically run on http://localhost:8911.

  2. Start ngrok: Open a new terminal window and run ngrok, telling it to forward traffic to your Redwood API port (8911):

    bash
    ngrok http 8911
  3. Get the ngrok URL: ngrok will display output similar to this:

    Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Forwarding https://<UNIQUE_ID>.ngrok-free.app -> http://localhost:8911 Forwarding http://<UNIQUE_ID>.ngrok-free.app -> http://localhost:8911 Web Interface http://127.0.0.1:4040

    Copy the https forwarding URL (e.g., https://<UNIQUE_ID>.ngrok-free.app). Using HTTPS is strongly recommended.

  4. Update .env: Modify the PLIVO_CALLBACK_BASE_URL in your .env file to use the ngrok HTTPS URL:

    dotenv
    # .env (update this line)
    PLIVO_CALLBACK_BASE_URL="https://<UNIQUE_ID>.ngrok-free.app" # Use your actual ngrok HTTPS URL
  5. Restart Redwood: Stop (Ctrl+C) and restart your Redwood dev server (yarn rw dev) to pick up the changed environment variable.

  6. Test Sending: Use Redwood's GraphQL Playground (usually http://localhost:8911/graphql) or a tool like Postman/curl to send the sendMessage mutation:

    Method: POST URL: http://localhost:8911/graphql Headers: Content-Type: application/json, auth-provider: dbAuth (or your auth header), etc. Body:

    json
    {
      "query": "mutation SendTestMessage($input: SendMessageInput!) { sendMessage(input: $input) { id plivoMessageId to status } }",
      "variables": {
        "input": {
          "to": "+15551234567", // Use a real phone number you can check
          "body": "RedwoodJS Plivo Callback Test - " + Date.now()
        }
      }
    }
  7. Observe Callbacks:

    • Check the terminal running yarn rw dev. You should see logs from the sendMessage service.
    • Wait a few seconds/minutes. Check the Redwood logs again. You should see logs from the plivoCallback function indicating receipt of the webhook and signature validation status.
    • Check the ngrok web interface (http://127.0.0.1:4040) to inspect the incoming requests from Plivo.
    • Check your database (e.g., using yarn rw prisma studio) to see the Message record being created and then its status field updating (e.g., from 'queued' to 'sent', then 'delivered' or 'failed').

6. Error Handling and Logging

  • Service Errors: The sendMessage service includes basic try...catch blocks. Log detailed errors using logger.error({ error, ...context }). Consider mapping specific Plivo API errors to user-friendly messages if necessary.
  • Callback Errors: The plivoCallback function logs errors during signature validation and payload processing. Returning appropriate HTTP status codes (403, 500, 200) helps Plivo manage retries.
  • Database Errors: Wrap database operations (db.message.create, db.message.update) in try...catch within both the service and the callback function to handle potential database connectivity or constraint issues. Log these errors clearly.
  • Logging Levels: Use Redwood's logger levels (info, warn, error) appropriately. info for standard operations, warn for unexpected but recoverable situations (like missing headers or message not found), error for failures.

7. Security Considerations

  • Signature Validation: This is the most critical security aspect. Never process a Plivo callback without successfully validating the X-Plivo-Signature-V3 header. This prevents attackers from sending fake or malicious status updates to your endpoint, ensuring the data genuinely originates from Plivo. Ensure PLIVO_AUTH_TOKEN is kept secret and is not exposed client-side.
  • Input Validation: Validate input in the sendMessage mutation (validate function) to prevent invalid data (e.g., malformed phone numbers, overly long messages) from reaching the service or Plivo.
  • Rate Limiting: While not implemented here, consider adding rate limiting to your sendMessage mutation endpoint if abuse is a concern. Plivo also has its own API rate limits.
  • Environment Variables: Never commit sensitive information like PLIVO_AUTH_TOKEN directly into your code repository. Use environment variables managed securely by your deployment platform.
  • HTTPS: Always use HTTPS for your callback URL (PLIVO_CALLBACK_BASE_URL), both locally with ngrok and especially in production. This encrypts the data in transit, protecting sensitive information within the callback payload.

8. Troubleshooting and Caveats

  • Callback Not Received:
    • Verify the PLIVO_CALLBACK_BASE_URL in your .env is correct (using https:// for ngrok/prod) and publicly accessible (check ngrok status or production deployment URL).
    • Ensure the URL passed in the sendMessage function's url parameter exactly matches the expected endpoint (<BASE_URL>/plivoCallback). Check the sendMessage logs.
    • Check Plivo's Message Logs in their console for errors related to sending the webhook (e.g., connection timeouts, HTTP errors).
    • Firewall issues might block incoming POST requests locally or on the server.
  • Signature Validation Failed (403 Error):
    • Double-check that PLIVO_AUTH_TOKEN in your .env exactly matches the one in your Plivo console.
    • Ensure the requestUrl constructed in plivoCallback.ts exactly matches the URL Plivo is calling (including https, the domain, and the path /plivoCallback). Check ngrok/server logs for the exact incoming request details.
    • Make sure you are passing the raw, unparsed request body string to Plivo.validateV3Signature. Middleware or framework auto-parsing might alter it before validation occurs. The current implementation passes event.body which should be the raw string in standard Lambda proxy integrations.
    • Verify the event.httpMethod is correctly passed (should be POST).
  • Message Not Found in DB (Callback Handler):
    • This can be a timing issue: the callback arrives before the sendMessage service finishes writing the initial record. Returning 200 OK allows Plivo to stop retrying while your system catches up. Add robust logging to monitor this.
    • Verify the MessageUUID from the Plivo callback payload matches the plivoMessageId stored in the database. Check for typos or case sensitivity issues.
  • Plivo Statuses and Error Codes: Familiarize yourself with Plivo's message statuses (queued, sent, delivered, undelivered, failed) and potential error codes to handle them appropriately in your application logic or UI. For example, an ErrorCode might indicate an invalid destination number or carrier issue. See Plivo Docs for Message States and Error Codes.
  • Payload Parsing: Be mindful of the Content-Type header (application/json vs application/x-www-form-urlencoded) sent by Plivo and ensure your parsing logic in the callback handler matches. Check Plivo documentation or inspect incoming requests if you encounter unexpected formats.

9. Deployment

  1. Choose a Hosting Provider: Deploy your RedwoodJS application to a platform like Vercel, Netlify, Render, or AWS Serverless (using Lambda for functions).
  2. Configure Environment Variables: Set the production values for DATABASE_URL, PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_SOURCE_NUMBER in your hosting provider's environment variable settings. Never hardcode these.
  3. Set Production Callback URL: Crucially, update PLIVO_CALLBACK_BASE_URL to your deployed API's public HTTPS URL. For example, if your API is deployed at https://api.myapp.com, set PLIVO_CALLBACK_BASE_URL="https://api.myapp.com". The callback function will then be accessible at https://api.myapp.com/plivoCallback.
  4. Deploy: Follow your hosting provider's instructions to deploy your RedwoodJS application. Ensure the API side (including the plivoCallback function) is deployed correctly and accessible.
  5. Test in Production: Send a test message using your production deployment and verify that the callback is received, the signature is validated, and the status is updated correctly in the production database. Monitor logs closely.

10. Verification and Testing

  • Unit/Integration Tests:
    • Write tests for the sendMessage service, mocking the Plivo client (plivo.messages.create) and db calls (db.message.create). Verify input validation and correct data saving.
    • Write tests for the plivoCallback function. Mock db calls (db.message.update). Create mock APIGatewayEvent objects with valid and invalid signatures (using jest.spyOn to mock Plivo.validateV3Signature), different payloads (delivered, failed, missing fields), and different Content-Types (application/json, application/x-www-form-urlencoded) to test validation and processing logic thoroughly.
  • Manual Verification Checklist:
    • Can send an SMS successfully via the GraphQL mutation?
    • Is the initial message record created in the database with status 'queued' and the correct plivoMessageId?
    • Is the Plivo callback request received by the plivoCallback function (check logs/ngrok/production function logs)?
    • Is the Plivo signature successfully validated (check logs for ""signature validated successfully"")?
    • Is the message status updated correctly in the database (e.g., to 'delivered' or 'failed')?
    • Is the plivoErrorCode stored correctly for failed/undelivered messages?
    • Does the system handle invalid signatures correctly (rejects request with 403, logs error)?
    • Does the system handle malformed callback payloads gracefully (logs error, returns appropriate status code like 200 or 400)?

This guide provides a robust foundation for integrating Plivo SMS sending and delivery status tracking into your RedwoodJS application. Remember to adapt the error handling, logging, and security measures to the specific needs and scale of your project. Happy coding!

Frequently Asked Questions

How to send SMS messages with RedwoodJS and Plivo?

You can send SMS messages by creating a RedwoodJS Service and a GraphQL mutation that uses the Plivo Node.js SDK. The service function will interact with the Plivo API to send messages and store message details in a database. The GraphQL mutation will provide an interface for your web application to trigger sending messages.

What is the Plivo callback mechanism in RedwoodJS?

The Plivo callback mechanism allows your RedwoodJS application to receive real-time delivery status updates for sent SMS messages. Plivo sends these updates as HTTP POST requests (webhooks) to a designated endpoint in your RedwoodJS application. This enables you to track message status (e.g., queued, sent, delivered, failed) and update your application accordingly.

Why does Plivo require signature validation for webhooks?

Plivo requires signature validation to ensure the security and authenticity of the callback requests. This verification step confirms that the webhook originated from Plivo and prevents malicious actors from spoofing status updates. The `X-Plivo-Signature-V3` header is used for this validation process.

How to set up Plivo SMS in a RedwoodJS project?

First, install the Plivo Node.js library. Then, configure environment variables for your Plivo Auth ID, Auth Token, source phone number, and callback URL. Set up a Prisma schema to store message data. Create a RedwoodJS Service to handle sending messages and a RedwoodJS Function to handle incoming Plivo webhooks.

How to use ngrok for local Plivo callback testing?

Use `ngrok http 8911` to create a public URL pointing to your local Redwood API server. Update the `PLIVO_CALLBACK_BASE_URL` environment variable with your ngrok HTTPS URL and restart your RedwoodJS server. Plivo can then send webhooks to your local machine during development.

What is the role of Prisma in handling Plivo messages?

Prisma is used as the Object-Relational Mapper (ORM) to interact with your database. It allows you to define your database schema (including a Message model) and easily perform database operations (create, update, query) within your RedwoodJS services and functions to store and retrieve message information.

When should I update the PLIVO_CALLBACK_BASE_URL?

Update `PLIVO_CALLBACK_BASE_URL` every time you start ngrok for local development. When deploying to production, update it to your deployed API's public HTTPS URL. This ensures that Plivo can always reach your webhook endpoint.

Can I test Plivo callbacks without a real phone number?

While you can set up the system and test sending messages, Plivo callbacks require sending to valid phone numbers. Use a real phone number you have access to during testing to observe actual delivery statuses from Plivo's network.

What is the correct format for the callback URL in RedwoodJS?

The callback URL should consist of your `PLIVO_CALLBACK_BASE_URL` followed by the path to your RedwoodJS function, typically `/plivoCallback`. For example: `https://your-api-url.com/plivoCallback`. This assumes you named your Redwood function `plivoCallback`.

Why does the Plivo callback function need to validate signatures?

Signature validation is crucial for security. It verifies that incoming webhook requests genuinely originate from Plivo and prevents unauthorized or malicious actors from tampering with your message status data. Without this check, your application could be vulnerable to attacks.

How to track Plivo message status updates in RedwoodJS?

You track message status by implementing a RedwoodJS function that handles the incoming Plivo webhook. This function parses the webhook payload, validates the signature, and updates the corresponding message's status in the database based on the data received from Plivo.

What are common Plivo webhook errors in RedwoodJS?

Common errors include invalid signatures (403 Forbidden) caused by incorrect configuration of the `PLIVO_AUTH_TOKEN` or `requestUrl`, and message not found issues, often due to timing discrepancies between the webhook arrival and the message creation in the database.

How to handle Plivo callback payload parsing issues?

The callback function should check the "Content-Type" header. Parse the payload as JSON if "Content-Type" is "application/json"; parse using "URLSearchParams" if "Content-Type" is "application/x-www-form-urlencoded". Include robust logging for debugging unexpected scenarios.

How to configure the Plivo client in RedwoodJS?

Initialize the Plivo client in your service file using `new Plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN)`. Ensure these environment variables are correctly set in your `.env` file and loaded into the RedwoodJS API side.