code examples
code examples
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:
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
endPrerequisites:
- 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).
ngrokinstalled 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.
# 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 plivoEnvironment 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".
# .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 ngrokPLIVO_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 usehttps://for ngrok or production.
RedwoodJS Configuration:
Ensure your redwood.toml includes the API environment variables:
# 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 = true2. 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:
// 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:
# Generate SQL migration files and apply to the database
yarn rw prisma migrate dev --name add_message_modelThis 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:
yarn rw g service messagesImplement 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.
// 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
sendMessagefunction takestoandbodyas input. - Crucially, we construct the
callbackUrlusingPLIVO_CALLBACK_BASE_URLand append/plivoCallback(this must match the filename of our webhook function later). This URL is passed in theurlparameter of theplivoClient.messages.createcall. - We handle the response from Plivo, extracting the
message_uuid. - We create a record in our
Messagetable with theplivoMessageIdand an initialstatusof'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.
// 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:
yarn rw g function plivoCallback --typescriptThis 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.
// 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-V3andX-Plivo-Signature-V3-Nonceheaders. - We reconstruct the full
requestUrlthat Plivo used when sending the webhook. - We use
Plivo.validateV3Signaturewith 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 Forbiddenresponse.
- We retrieve the
- Payload Processing:
- The code now explicitly checks the
Content-Typeheader to decide between parsing JSON orx-www-form-urlencodeddata (usingURLSearchParams). It also handles cases where the body might already be parsed by the environment. - It extracts
MessageUUID,MessageStatus, andErrorCodefrom the payload.
- The code now explicitly checks the
- Database Update:
- We use
db.message.updatewith awhereclause targeting theplivoMessageId(which corresponds to Plivo'sMessageUUID). - We update the
statusandplivoErrorCode.
- We use
- Response: We return a
200 OKto Plivo to acknowledge successful receipt and processing. If signature validation fails, return403. If payload processing fails, return500to potentially trigger Plivo retries (check Plivo's retry policy). Return200even 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:
-
Start your Redwood development server:
bashyarn rw devYour API server will typically run on
http://localhost:8911. -
Start ngrok: Open a new terminal window and run ngrok, telling it to forward traffic to your Redwood API port (8911):
bashngrok http 8911 -
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:4040Copy the
httpsforwarding URL (e.g.,https://<UNIQUE_ID>.ngrok-free.app). Using HTTPS is strongly recommended. -
Update
.env: Modify thePLIVO_CALLBACK_BASE_URLin your.envfile 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 -
Restart Redwood: Stop (
Ctrl+C) and restart your Redwood dev server (yarn rw dev) to pick up the changed environment variable. -
Test Sending: Use Redwood's GraphQL Playground (usually
http://localhost:8911/graphql) or a tool like Postman/curl to send thesendMessagemutation:Method:
POSTURL:http://localhost:8911/graphqlHeaders: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() } } } -
Observe Callbacks:
- Check the terminal running
yarn rw dev. You should see logs from thesendMessageservice. - Wait a few seconds/minutes. Check the Redwood logs again. You should see logs from the
plivoCallbackfunction 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 theMessagerecord being created and then itsstatusfield updating (e.g., from 'queued' to 'sent', then 'delivered' or 'failed').
- Check the terminal running
6. Error Handling and Logging
- Service Errors: The
sendMessageservice includes basictry...catchblocks. Log detailed errors usinglogger.error({ error, ...context }). Consider mapping specific Plivo API errors to user-friendly messages if necessary. - Callback Errors: The
plivoCallbackfunction 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) intry...catchwithin 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.infofor standard operations,warnfor unexpected but recoverable situations (like missing headers or message not found),errorfor 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-V3header. This prevents attackers from sending fake or malicious status updates to your endpoint, ensuring the data genuinely originates from Plivo. EnsurePLIVO_AUTH_TOKENis kept secret and is not exposed client-side. - Input Validation: Validate input in the
sendMessagemutation (validatefunction) 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
sendMessagemutation endpoint if abuse is a concern. Plivo also has its own API rate limits. - Environment Variables: Never commit sensitive information like
PLIVO_AUTH_TOKENdirectly 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_URLin your.envis correct (usinghttps://for ngrok/prod) and publicly accessible (check ngrok status or production deployment URL). - Ensure the URL passed in the
sendMessagefunction'surlparameter exactly matches the expected endpoint (<BASE_URL>/plivoCallback). Check thesendMessagelogs. - 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.
- Verify the
- Signature Validation Failed (403 Error):
- Double-check that
PLIVO_AUTH_TOKENin your.envexactly matches the one in your Plivo console. - Ensure the
requestUrlconstructed inplivoCallback.tsexactly matches the URL Plivo is calling (includinghttps, 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 passesevent.bodywhich should be the raw string in standard Lambda proxy integrations. - Verify the
event.httpMethodis correctly passed (should bePOST).
- Double-check that
- Message Not Found in DB (Callback Handler):
- This can be a timing issue: the callback arrives before the
sendMessageservice finishes writing the initial record. Returning200 OKallows Plivo to stop retrying while your system catches up. Add robust logging to monitor this. - Verify the
MessageUUIDfrom the Plivo callback payload matches theplivoMessageIdstored in the database. Check for typos or case sensitivity issues.
- This can be a timing issue: the callback arrives before the
- 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, anErrorCodemight indicate an invalid destination number or carrier issue. See Plivo Docs for Message States and Error Codes. - Payload Parsing: Be mindful of the
Content-Typeheader (application/jsonvsapplication/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
- Choose a Hosting Provider: Deploy your RedwoodJS application to a platform like Vercel, Netlify, Render, or AWS Serverless (using Lambda for functions).
- Configure Environment Variables: Set the production values for
DATABASE_URL,PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPLIVO_SOURCE_NUMBERin your hosting provider's environment variable settings. Never hardcode these. - Set Production Callback URL: Crucially, update
PLIVO_CALLBACK_BASE_URLto your deployed API's public HTTPS URL. For example, if your API is deployed athttps://api.myapp.com, setPLIVO_CALLBACK_BASE_URL="https://api.myapp.com". The callback function will then be accessible athttps://api.myapp.com/plivoCallback. - Deploy: Follow your hosting provider's instructions to deploy your RedwoodJS application. Ensure the API side (including the
plivoCallbackfunction) is deployed correctly and accessible. - 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
sendMessageservice, mocking the Plivo client (plivo.messages.create) anddbcalls (db.message.create). Verify input validation and correct data saving. - Write tests for the
plivoCallbackfunction. Mockdbcalls (db.message.update). Create mockAPIGatewayEventobjects with valid and invalid signatures (usingjest.spyOnto mockPlivo.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.
- Write tests for the
- 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
plivoCallbackfunction (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
plivoErrorCodestored 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.