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
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".
.env
# .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 = 8910apiUrl = "/.redwood/functions"# Default Redwood settingincludeEnvironmentVariables = []
[api]port = 8911host = "localhost"# Make Plivo env vars available to the API sideincludeEnvironmentVariables = ["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.tsimport { db } from'src/lib/db'import { logger } from'src/lib/logger'import { validate } from'@redwoodjs/api'importPlivofrom'plivo'// Import the Plivo SDK// Initialize Plivo client (ensure env vars are loaded)const plivoClient = newPlivo.Client(
process.env.PLIVO_AUTH_ID,
process.env.PLIVO_AUTH_TOKEN
)
interfaceSendMessageInput {
to: stringbody: string
}
exportconstsendMessage = async ({ input }: { input: SendMessageInput }) => {
validate(input.to, 'Recipient Number', { presence: true })
// Add more specific phone number validation if neededvalidate(input.body, 'Message Body', { presence: true, length: { max: 1600 } })
const { to, body } = input
const sourceNumber = process.env.PLIVO_SOURCE_NUMBERconst callbackBaseUrl = process.env.PLIVO_CALLBACK_BASE_URLif (!sourceNumber) {
logger.error('PLIVO_SOURCE_NUMBER environment variable is not set.')
thrownewError('SMS configuration error: Missing source number.')
}
if (!callbackBaseUrl) {
logger.error('PLIVO_CALLBACK_BASE_URL environment variable is not set.')
thrownewError('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 endpointconst callbackUrl = `${callbackBaseUrl}/plivoCallback`// Matches our function nametry {
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 callbacksurl: 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 messagesif (response.messageUuid && response.messageUuid.length > 0) {
const plivoMessageId = response.messageUuid[0]
// Save the initial message state to the databaseconst 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')
thrownewError('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/typesthrownewError(`Failed to send SMS: ${error.message || 'Unknown Plivo error'}`)
}
}
// Optional: Add service functions to retrieve messages if neededexportconstmessages = () => {
return db.message.findMany({ orderBy: { createdAt: 'desc' } })
}
exportconstmessage = ({ 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.tsexportconst schema = gql`type Message {id: Int!plivoMessageId: String!to: String!body: String!status: String!plivoErrorCode: Int
createdAt: DateTime!updatedAt: DateTime!}typeQuery{messages:[Message!]!@requireAuth
message(id: Int!): Message @requireAuth}input SendMessageInput {to: String!body: String!}typeMutation{
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.tsimporttype { APIGatewayEvent, Context } from'aws-lambda'import { logger } from'src/lib/logger'import { db } from'src/lib/db'importPlivofrom'plivo'// Import Plivo to use the signature validation utilityimport { 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
*/exportconsthandler = 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_TOKENif (!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 validationconst 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 errorreturn { statusCode: 500, body: 'Internal Server Error during validation.' }
}
// --- End Security Check ---// --- Process the Callback Data ---letpayload: 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');
} elseif (contentType.includes('application/x-www-form-urlencoded')) {
// Use URLSearchParams for robust form data parsing
payload = Object.fromEntries(newURLSearchParams(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);
}
} elseif (event.body && typeof event.body === 'object'){
// Body might be pre-parsed by the environment (e.g., Lambda Proxy integration)
payload = event.bodyasRecord<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 bodyreturn { 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) : nullif (!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 statusconst updatedMessage = await db.message.update({
where: { plivoMessageId: plivoMessageId },
data: {
status: status, // e.g., 'sent', 'delivered', 'failed', 'undelivered'plivoErrorCode: errorCode,
updatedAt: newDate(), // 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 Plivoreturn {
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:
Start your Redwood development server:
Bash
yarn rw dev
Your 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):
Bash
ngrok http 8911
Get the ngrok URL: ngrok will display output similar to this:
Code
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.
Update .env:
Modify the PLIVO_CALLBACK_BASE_URL in your .env file to use the ngrok HTTPS URL:
.env
# .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 the sendMessage mutation:
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 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
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, and PLIVO_SOURCE_NUMBER in your hosting provider's environment variable settings. Never hardcode these.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Technologies Used:
System Architecture:
Prerequisites:
ngrokinstalled for local development testing.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
.envfile 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".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 usehttps://for ngrok or production.RedwoodJS Configuration:
Ensure your
redwood.tomlincludes 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.prismaand add aMessagemodel: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/migrationsand 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:
Implement the
sendMessageService Function:Edit
api/src/services/messages/messages.tsto 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 } }) }sendMessagefunction takestoandbodyas input.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.message_uuid.Messagetable with theplivoMessageIdand an initialstatusof'queued'.Define the GraphQL Schema (SDL):
Update
api/src/graphql/messages.sdl.tsto 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
Messagetype mirroring our Prisma model, a query to fetch messages, and thesendMessagemutation which accepts theSendMessageInputand returns the createdMessage. We've added@requireAuthas a placeholder; adjust authentication as needed for your application.4. Building the Plivo Callback API Endpoint
Plivo will send HTTP POST requests to the
urlwe 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.tsto 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 } }X-Plivo-Signature-V3andX-Plivo-Signature-V3-Nonceheaders.requestUrlthat Plivo used when sending the webhook.Plivo.validateV3Signaturewith the request method (POST), URL, nonce, your Auth Token, the received signature, and the raw request body string.403 Forbiddenresponse.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.MessageUUID,MessageStatus, andErrorCodefrom the payload.db.message.updatewith awhereclause targeting theplivoMessageId(which corresponds to Plivo'sMessageUUID).statusandplivoErrorCode.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.
ngrokis perfect for this.Steps:
Start your Redwood development server:
Your 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):
Get the ngrok URL: ngrok will display output similar to this:
Copy 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: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:{ "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:
yarn rw dev. You should see logs from thesendMessageservice.plivoCallbackfunction indicating receipt of the webhook and signature validation status.http://127.0.0.1:4040) to inspect the incoming requests from Plivo.yarn rw prisma studio) to see theMessagerecord being created and then itsstatusfield updating (e.g., from 'queued' to 'sent', then 'delivered' or 'failed').6. Error Handling and Logging
sendMessageservice includes basictry...catchblocks. Log detailed errors usinglogger.error({ error, ...context }). Consider mapping specific Plivo API errors to user-friendly messages if necessary.plivoCallbackfunction logs errors during signature validation and payload processing. Returning appropriate HTTP status codes (403,500,200) helps Plivo manage retries.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.info,warn,error) appropriately.infofor standard operations,warnfor unexpected but recoverable situations (like missing headers or message not found),errorfor failures.7. Security Considerations
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.sendMessagemutation (validatefunction) to prevent invalid data (e.g., malformed phone numbers, overly long messages) from reaching the service or Plivo.sendMessagemutation endpoint if abuse is a concern. Plivo also has its own API rate limits.PLIVO_AUTH_TOKENdirectly into your code repository. Use environment variables managed securely by your deployment platform.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
PLIVO_CALLBACK_BASE_URLin your.envis correct (usinghttps://for ngrok/prod) and publicly accessible (check ngrok status or production deployment URL).sendMessagefunction'surlparameter exactly matches the expected endpoint (<BASE_URL>/plivoCallback). Check thesendMessagelogs.PLIVO_AUTH_TOKENin your.envexactly matches the one in your Plivo console.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.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.event.httpMethodis correctly passed (should bePOST).sendMessageservice finishes writing the initial record. Returning200 OKallows Plivo to stop retrying while your system catches up. Add robust logging to monitor this.MessageUUIDfrom the Plivo callback payload matches theplivoMessageIdstored in the database. Check for typos or case sensitivity issues.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.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
DATABASE_URL,PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN, andPLIVO_SOURCE_NUMBERin your hosting provider's environment variable settings. Never hardcode these.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.plivoCallbackfunction) is deployed correctly and accessible.10. Verification and Testing
sendMessageservice, mocking the Plivo client (plivo.messages.create) anddbcalls (db.message.create). Verify input validation and correct data saving.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.plivoMessageId?plivoCallbackfunction (check logs/ngrok/production function logs)?plivoErrorCodestored correctly for failed/undelivered messages?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!