Send SMS marketing campaigns using Plivo's messaging API and RedwoodJS's full-stack JavaScript framework. This comprehensive guide demonstrates how to integrate Plivo SMS with RedwoodJS, covering GraphQL API implementation, Prisma database setup, React component development, and production deployment strategies.
Learn how to build a production-ready SMS campaign system that combines RedwoodJS's GraphQL mutations with Plivo's reliable SMS delivery infrastructure. This tutorial covers authentication, error handling with automatic retries, phone number validation, and campaign logging – everything you need for enterprise-grade SMS marketing functionality.
What you'll build in this RedwoodJS Plivo integration:
GraphQL mutations for sending SMS messages via Plivo API
Prisma ORM schema for SMS campaign logging and analytics
Automatic error handling with retry mechanisms
React frontend components for campaign management
Production-ready deployment with environment security
Requirements:
Node.js 20.x or higher
Yarn 1.22.21 or higher
RedwoodJS 8.x (this guide uses current best practices)
Quick Summary: This guide shows you how to integrate Plivo SMS API with RedwoodJS to build SMS marketing campaigns. You'll set up GraphQL mutations, implement Prisma database logging, add React frontend components, configure error handling with retries, and deploy to production. The complete implementation takes approximately 2–3 hours and requires Node.js 20+, Yarn, and a Plivo account.
Prerequisites for Plivo SMS Integration
Before integrating Plivo with RedwoodJS, ensure you have:
Node.js (>=18.x recommended, tested with v18.18.0, check RedwoodJS docs for current requirements)
You'll need a Plivo phone number capable of sending SMS messages. (Note: Trial accounts have restrictions).
Basic understanding of RedwoodJS concepts (sides, cells, services, GraphQL).
Familiarity with command-line tools.
Expected Outcome:
A RedwoodJS application with a page containing a form. Submitting the form (with a recipient phone number and message text) triggers a backend function that uses the Plivo API to send the SMS. A record of the attempt (success or failure) is logged in the database.
Set Up Your RedwoodJS Project
Create a new RedwoodJS application if you don't have one yet:
Bash
yarn create redwood-app my-plivo-app
cd my-plivo-app
Install the Plivo Node.js SDK (version 4.74.0 is the latest as of October 2025):
Bash
yarn workspace api add plivo
The Plivo SDK package name is plivo on npm – this is the official, maintained version of the SDK.
The Auth ID and Auth Token are displayed prominently on the main dashboard overview page.
Navigate to Messaging -> Phone Numbers to find or purchase a Plivo number to use as the PLIVO_SOURCE_NUMBER. Ensure it's SMS-enabled for your target region.
IMPORTANT: Add .env to your .gitignore file (RedwoodJS usually includes this by default) to prevent committing secrets.
Text
# .gitignore
# ... other entries
.env
# ...
Purpose: Storing sensitive credentials in environment variables is a standard security practice. RedwoodJS automatically loads variables from the .env file into process.env.
Ensure your basic RedwoodJS development environment works:
Bash
yarn redwood dev
This should start the development servers for both the api and web sides and open your browser to http://localhost:8910. Stop the server (Ctrl+C) once confirmed.
Create the Database Schema
Define a CampaignLog model to track all SMS campaigns. Open api/db/schema.prisma and add:
Prisma
model CampaignLog {
id Int @id @default(autoincrement())
to String
text String
plivoMessageId String?
status String // SENT, FAILED, PENDING
errorDetails String?
sentAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([sentAt])
}
Index strategy:
@@index([status]) – speeds up queries filtering by message status
@@index([sentAt]) – optimizes date-range queries for campaign analytics
These indexes follow Prisma best practices for production databases. Indexes on frequently queried fields improve performance as your campaign logs grow.
Run the migration to create the table:
Bash
yarn rw prisma migrate dev --name create_campaign_log
Entity Relationship Diagram (ERD):
For this simple case, we only have one model.
erDiagram
CampaignLog {
Int id PK
String to
String text
String plivoMessageId NULL
String status // e.g., 'SENT', 'FAILED', 'PENDING'
String errorDetails NULL
DateTime sentAt
DateTime createdAt
DateTime updatedAt
}
PlivoMessageResult: Defines the structure of the data returned after attempting to send a message. Includes success status, the Plivo message UUID if successful, or an error message if not.
SendPlivoMessageInput: Defines the required input fields for the mutation.
Mutation.sendPlivoMessage: Declares the mutation endpoint.
@requireAuth: For security, we require authentication for this mutation. This ensures only logged-in users can send messages. In a production application, you should always use @requireAuth for sensitive operations.
Implement the SMS Service
Create the service that sends SMS messages via Plivo's API:
TypeScript
// api/src/services/plivoCampaign/plivoCampaign.tsimport * as plivo from'plivo'import retry from'async-retry'importtype { MutationResolvers } from'types/graphql'import { logger } from'src/lib/logger'import { db } from'src/lib/db'exportconstsendPlivoMessage: MutationResolvers['sendPlivoMessage'] = async ({ input }) => {
const { to, text } = input
const authId = process.env.PLIVO_AUTH_IDconst authToken = process.env.PLIVO_AUTH_TOKENconst sourceNumber = process.env.PLIVO_SOURCE_NUMBERif (!authId || !authToken || !sourceNumber) {
logger.error('Plivo credentials or source number missing in .env file.')
return {
success: false,
error: 'Server configuration error: Plivo credentials missing.',
}
}
// Basic input validationif (!to || !text || text.trim() === '') {
return { success: false, error: 'Recipient number and message text cannot be empty.' }
}
if (!/^\+?[1-9]\d{1,14}$/.test(to)) {
return { success: false, error: 'Invalid recipient phone number format. Use E.164 format (e.g., +14155552671).' }
}
const client = new plivo.Client(authId, authToken);
let logEntry;
try {
// Log attempt before trying
logEntry = await db.campaignLog.create({
data: { to, text, status: 'PENDING', sentAt: newDate() }
})
logger.info(`Attempting to send Plivo message to: ${to} (Log ID: ${logEntry.id})`);
const response = awaitretry(
async (bail, attempt) => {
logger.debug(`Retry attempt ${attempt} for message to ${to} (Log ID: ${logEntry.id})`);
returnawait client.messages.create(sourceNumber, to, text);
},
{
retries: 3,
factor: 2,
minTimeout: 1000,
onRetry: (error, attempt) => {
logger.warn(
`Retrying Plivo send (${attempt}/${3}) for ${to} (Log ID: ${logEntry.id}) due to error: ${error.message}`
)
},
}
);
// Update log on successawait db.campaignLog.update({
where: { id: logEntry.id },
data: {
status: 'SENT',
plivoMessageId: response.messageUuid[0],
},
})
logger.info(`Plivo message sent successfully to ${to}. Message UUID: ${response.messageUuid[0]} (Log ID: ${logEntry.id})`);
return {
success: true,
messageId: response.messageUuid[0],
error: null,
};
} catch (error) {
logger.error({ error, logId: logEntry?.id }, `Failed to send Plivo message to ${to} after retries`);
// Update log on failureif (logEntry) {
await db.campaignLog.update({
where: { id: logEntry.id },
data: {
status: 'FAILED',
errorDetails: error.message || 'Unknown error after retries',
},
});
}
return {
success: false,
error: `Failed to send message after retries: ${error.message || 'Unknown error'}`,
};
}
}
Install the retry library:
Bash
yarn workspace api add async-retry @types/async-retry
How this works:
Initialize the Plivo client with your credentials
Validate input (phone format and message content)
Create a database log entry with PENDING status
Call client.messages.create() with automatic retry logic (3 attempts with exponential backoff)
Update database log with SENT status and Plivo message UUID on success
Catch errors, log failed attempts with error details, and update database with FAILED status
Return structured results to the GraphQL client
The message UUID from Plivo allows you to track delivery status later.
Validate Phone Numbers (E.164 Format)
Plivo requires phone numbers in E.164 format. This international standard includes the country code and removes all formatting characters.
E.164 format rules:
Start with + followed by country code
Include area code and number without spaces, dashes, or parentheses
Maximum 15 digits (including country code)
Examples:
✅ +14155552671 (US number)
✅ +442071838750 (UK number)
✅ +551155256325 (Brazil number)
❌ (415) 555-2671 (contains formatting)
❌ 4155552671 (missing country code)
The validation logic is already included in the service implementation above. The regex /^\+?[1-9]\d{1,14}$/ checks for valid E.164 format.
Why E.164 matters: Using the standard format ensures your messages reach the correct recipients across any country. It eliminates ambiguity in phone number interpretation.
E.164 format validation with helpful error messages
Loading state prevents duplicate submissions
Toast notifications for success and error feedback
Form clears after successful send
Client-side and server-side validation
Add Error Handling and Retries
The service implementation above already includes comprehensive error handling with automatic retries using the async-retry library.
Retry configuration explained:
retries: 3 – attempts the request up to 3 times before failing
factor: 2 – exponential backoff (2x delay between retries)
minTimeout: 1000 – waits 1 second before first retry
Why retry matters: Network issues or temporary Plivo API failures can cause messages to fail. Implementing retries improves reliability without requiring manual intervention.
When NOT to retry: The current implementation retries all errors. In production, you may want to bail immediately on validation errors (400 status codes) by inspecting the error structure Plivo returns.
Why this matters: Without authentication, anyone who discovers your API endpoint could send SMS messages using your Plivo account. This leads to unauthorized charges and potential abuse.
Add environment variables in the Netlify dashboard:
Navigate to Site settings → Environment variables
Add each variable: PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SOURCE_NUMBER
Click "Save"
For AWS/Render/Fly.io:
Consult your platform's documentation for setting environment variables. Each platform has a different method, but all require the same three variables.
Deploy your application:
Bash
yarn rw deploy
Post-deployment checklist:
Verify environment variables are set correctly
Test sending an SMS through the production URL
Check your Plivo dashboard for message logs
Monitor your database for campaign log entries
Set up error monitoring (Sentry, Rollbar, or similar)
Production recommendations:
Enable Plivo's delivery receipts to track message status
Plivo charges per SMS segment, with pricing varying by destination country. US SMS typically costs $0.0075–$0.01 per segment. Check Plivo's pricing page for current rates in your target countries. A standard SMS segment is 160 characters.
What is E.164 phone number format?
E.164 is the international phone number format required by Plivo. It starts with + followed by country code and number without spaces or formatting (e.g., +14155552671 for a US number). Maximum 15 digits total. See our complete E.164 format guide.
Can I send bulk SMS campaigns with RedwoodJS and Plivo?
Yes. Modify the GraphQL mutation to accept an array of phone numbers and implement batch sending with rate limiting. Consider using a queue system like Bull or Graphile Worker for large campaigns to prevent API throttling. For high-volume sending, you'll also need to register for 10DLC.
How do I track SMS delivery status with Plivo?
Plivo returns a message UUID for each sent SMS. Use this UUID to query the Plivo API for delivery status, or implement webhook handlers to receive real-time delivery notifications when messages are delivered or fail. Check the Plivo delivery reports documentation.
Does RedwoodJS support other SMS providers besides Plivo?
Yes. RedwoodJS works with any SMS provider with a Node.js SDK. Popular alternatives include Twilio, Vonage, MessageBird, and Sinch. The integration pattern remains similar – initialize the client in your service and call the provider's API.
What Node.js version does RedwoodJS require?
RedwoodJS 8.x requires Node.js 20.x or higher. Some deploy targets like AWS Lambda may have restrictions with Node.js 21+. Use the LTS version (20.x) for best compatibility.
How do I handle SMS opt-out requests?
Create a database table to track opt-out requests. Before sending messages, query this table to filter out opted-out numbers. Include an opt-out message in your campaigns (e.g., "Reply STOP to unsubscribe") and implement webhook handlers to process replies. See our SMS compliance guide for TCPA requirements.
Next Steps: Enhance Your SMS Marketing System
You've built a complete SMS marketing campaign system with Plivo and RedwoodJS. Your implementation includes GraphQL mutations, database logging, error handling, and automatic retries.
Advanced features to add:
Bulk SMS campaigns and scheduling:
Modify your GraphQL mutation to accept arrays of phone numbers for batch sending
Implement queue systems (Bull, BullMQ, or Graphile Worker) for scheduled campaigns
Add rate limiting to comply with Plivo's API throughput limits
Create campaign templates with variable substitution for personalization
SMS analytics and reporting:
Build dashboards showing delivery rates, failure analysis, and cost tracking
Implement webhook handlers for real-time delivery status updates
Track campaign ROI with conversion tracking and A/B testing
Generate reports on optimal send times and engagement metrics
Compliance and subscriber management:
Create opt-in/opt-out subscription systems with double opt-in confirmation
Implement automatic STOP/START keyword processing
Add TCPA and GDPR compliance features for US and EU markets
Build subscriber segmentation for targeted campaigns
Integration enhancements:
Connect with CRM systems (Salesforce, HubSpot) for contact synchronization
Implement two-way SMS conversations with automated responses
Add MMS support for multimedia marketing campaigns
Integrate with analytics platforms (Google Analytics, Segment)
About this guide: Last updated October 2025 with RedwoodJS 8.x, Plivo Node.js SDK 4.74.0, and Node.js 20.x requirements. Code examples tested on production deployments with Netlify, Vercel, and AWS Lambda.
Frequently asked questions
Integrate Plivo's messaging API into your RedwoodJS application. This involves installing the Plivo Node.js SDK, setting up environment variables for your Plivo credentials, creating a GraphQL mutation, and building a frontend form to trigger the message sending process. The provided tutorial guides you through each step to create a fully functional SMS sending feature within your RedwoodJS app.
Plivo is a cloud communications platform used to send SMS messages programmatically from your RedwoodJS application. It acts as the SMS gateway, removing the need for you to build that infrastructure yourself. The Plivo Node.js SDK facilitates the integration within your RedwoodJS service.
RedwoodJS provides a full-stack JavaScript/TypeScript framework with integrated frontend (React) and backend (GraphQL API, Prisma) capabilities, simplifying the development process. This integration allows you to build and deploy SMS campaigns easily by combining the Plivo API with a user-friendly interface.
Create a .env file in the root of your RedwoodJS project and add your PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_SOURCE_NUMBER. RedwoodJS loads these into process.env. Never hardcode these credentials directly in your code. Be sure to add .env to your .gitignore file to prevent exposing secrets.
Log in to your Plivo console at https://console.plivo.com/. Your Auth ID and Auth Token are displayed on the main dashboard overview page. Your Plivo source number can be found under 'Messaging -> Phone Numbers'.
The plivoCampaign service is responsible for interacting with the Plivo SDK. It handles authentication, message sending, and logging. This service isolates the Plivo-specific logic from the rest of your RedwoodJS application, promoting code organization and maintainability.
Use try...catch blocks around Plivo API calls to handle potential errors such as invalid credentials or network issues. Log error details using Redwood's logger and return user-friendly error messages in the GraphQL response. The example code also shows how to implement retries using the async-retry library for transient errors.
Prisma, the ORM used by RedwoodJS, allows you to log sent messages and their statuses (sent, failed, pending) in a database. This provides a record of all SMS activity, enabling tracking and analysis of your campaign's performance.
You can test using the GraphQL Playground at http://localhost:8910/graphql during development or through the form on the /send-campaign page. Test with valid and invalid phone numbers and messages to verify success and error handling. For more in-depth testing, mock the Plivo client in unit tests.
Use the E.164 format for phone numbers, such as +14155552671. This international standard ensures compatibility and successful message delivery. Client-side and server-side validation should be in place to enforce this format in your application.
Never commit .env to version control. When deploying, use your hosting provider's secure environment variable mechanisms (e.g., Netlify, Vercel, or Fly.io secrets) to store your Plivo API keys. This keeps sensitive information out of your codebase and protects your credentials.
Use a library like async-retry. Wrap the client.messages.create call within the retry function, configuring the number of attempts and the backoff strategy. This ensures that temporary network or server errors don't permanently prevent your messages from being sent. Examine Plivo's error responses for more specific retry criteria.
The request takes a JSON object with an input field containing the recipient's phone number (to in E.164 format) and the message text (text). For example: {"input": {"to": "+14155552671", "text": "Hello from Redwood!"}}
The response will be a JSON object indicating success or failure. A successful response includes a messageId (Plivo's UUID for the message). A failed response includes an error message. Example success: {"data": {"sendPlivoMessage": {"success": true, "messageId": "UUID", "error": null}}}. Example failure: {"data": {"sendPlivoMessage": {"success": false, "messageId": null, "error": "Error message"}}}
Send SMS marketing campaigns using Plivo's messaging API and RedwoodJS's full-stack JavaScript framework. This comprehensive guide demonstrates how to integrate Plivo SMS with RedwoodJS, covering GraphQL API implementation, Prisma database setup, React component development, and production deployment strategies.
Learn how to build a production-ready SMS campaign system that combines RedwoodJS's GraphQL mutations with Plivo's reliable SMS delivery infrastructure. This tutorial covers authentication, error handling with automatic retries, phone number validation, and campaign logging – everything you need for enterprise-grade SMS marketing functionality.
What you'll build in this RedwoodJS Plivo integration:
Requirements:
Table of Contents
Quick Summary: This guide shows you how to integrate Plivo SMS API with RedwoodJS to build SMS marketing campaigns. You'll set up GraphQL mutations, implement Prisma database logging, add React frontend components, configure error handling with retries, and deploy to production. The complete implementation takes approximately 2–3 hours and requires Node.js 20+, Yarn, and a Plivo account.
Prerequisites for Plivo SMS Integration
Before integrating Plivo with RedwoodJS, ensure you have:
Expected Outcome:
A RedwoodJS application with a page containing a form. Submitting the form (with a recipient phone number and message text) triggers a backend function that uses the Plivo API to send the SMS. A record of the attempt (success or failure) is logged in the database.
Set Up Your RedwoodJS Project
Create a new RedwoodJS application if you don't have one yet:
yarn create redwood-app my-plivo-app cd my-plivo-appInstall the Plivo Node.js SDK (version 4.74.0 is the latest as of October 2025):
The Plivo SDK package name is
plivoon npm – this is the official, maintained version of the SDK.Related guides:
Configure Your Environment Variables
Plivo requires an Auth ID and Auth Token for authentication. Store these securely using environment variables.
Create a
.envfile in the root of your project (if it doesn't exist).Add your Plivo credentials and a Plivo source phone number to the
.envfile:How to find Plivo Credentials:
Messaging -> Phone Numbersto find or purchase a Plivo number to use as thePLIVO_SOURCE_NUMBER. Ensure it's SMS-enabled for your target region.IMPORTANT: Add
.envto your.gitignorefile (RedwoodJS usually includes this by default) to prevent committing secrets.Purpose: Storing sensitive credentials in environment variables is a standard security practice. RedwoodJS automatically loads variables from the
.envfile intoprocess.env.Ensure your basic RedwoodJS development environment works:
This should start the development servers for both the
apiandwebsides and open your browser tohttp://localhost:8910. Stop the server (Ctrl+C) once confirmed.Create the Database Schema
Define a
CampaignLogmodel to track all SMS campaigns. Openapi/db/schema.prismaand add:Index strategy:
@@index([status])– speeds up queries filtering by message status@@index([sentAt])– optimizes date-range queries for campaign analyticsThese indexes follow Prisma best practices for production databases. Indexes on frequently queried fields improve performance as your campaign logs grow.
Run the migration to create the table:
Entity Relationship Diagram (ERD): For this simple case, we only have one model.
Define Your GraphQL Schema
Define the mutation input and output types:
// api/src/graphql/plivoCampaign.sdl.ts export const schema = gql` type PlivoMessageResult { success: Boolean! messageId: String error: String } input SendPlivoMessageInput { to: String! text: String! } type Mutation { sendPlivoMessage(input: SendPlivoMessageInput!): PlivoMessageResult! @requireAuth } `Generate the necessary GraphQL and service files:
This creates:
api/src/graphql/plivoCampaign.sdl.tsapi/src/services/plivoCampaign/plivoCampaign.tsapi/src/services/plivoCampaign/plivoCampaign.test.tsPlivoMessageResult: Defines the structure of the data returned after attempting to send a message. Includes success status, the Plivo message UUID if successful, or an error message if not.SendPlivoMessageInput: Defines the required input fields for the mutation.Mutation.sendPlivoMessage: Declares the mutation endpoint.@requireAuth: For security, we require authentication for this mutation. This ensures only logged-in users can send messages. In a production application, you should always use@requireAuthfor sensitive operations.Implement the SMS Service
Create the service that sends SMS messages via Plivo's API:
// api/src/services/plivoCampaign/plivoCampaign.ts import * as plivo from 'plivo' import retry from 'async-retry' import type { MutationResolvers } from 'types/graphql' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' export const sendPlivoMessage: MutationResolvers['sendPlivoMessage'] = async ({ input }) => { const { to, text } = input const authId = process.env.PLIVO_AUTH_ID const authToken = process.env.PLIVO_AUTH_TOKEN const sourceNumber = process.env.PLIVO_SOURCE_NUMBER if (!authId || !authToken || !sourceNumber) { logger.error('Plivo credentials or source number missing in .env file.') return { success: false, error: 'Server configuration error: Plivo credentials missing.', } } // Basic input validation if (!to || !text || text.trim() === '') { return { success: false, error: 'Recipient number and message text cannot be empty.' } } if (!/^\+?[1-9]\d{1,14}$/.test(to)) { return { success: false, error: 'Invalid recipient phone number format. Use E.164 format (e.g., +14155552671).' } } const client = new plivo.Client(authId, authToken); let logEntry; try { // Log attempt before trying logEntry = await db.campaignLog.create({ data: { to, text, status: 'PENDING', sentAt: new Date() } }) logger.info(`Attempting to send Plivo message to: ${to} (Log ID: ${logEntry.id})`); const response = await retry( async (bail, attempt) => { logger.debug(`Retry attempt ${attempt} for message to ${to} (Log ID: ${logEntry.id})`); return await client.messages.create(sourceNumber, to, text); }, { retries: 3, factor: 2, minTimeout: 1000, onRetry: (error, attempt) => { logger.warn( `Retrying Plivo send (${attempt}/${3}) for ${to} (Log ID: ${logEntry.id}) due to error: ${error.message}` ) }, } ); // Update log on success await db.campaignLog.update({ where: { id: logEntry.id }, data: { status: 'SENT', plivoMessageId: response.messageUuid[0], }, }) logger.info(`Plivo message sent successfully to ${to}. Message UUID: ${response.messageUuid[0]} (Log ID: ${logEntry.id})`); return { success: true, messageId: response.messageUuid[0], error: null, }; } catch (error) { logger.error({ error, logId: logEntry?.id }, `Failed to send Plivo message to ${to} after retries`); // Update log on failure if (logEntry) { await db.campaignLog.update({ where: { id: logEntry.id }, data: { status: 'FAILED', errorDetails: error.message || 'Unknown error after retries', }, }); } return { success: false, error: `Failed to send message after retries: ${error.message || 'Unknown error'}`, }; } }Install the retry library:
How this works:
client.messages.create()with automatic retry logic (3 attempts with exponential backoff)The message UUID from Plivo allows you to track delivery status later.
Validate Phone Numbers (E.164 Format)
Plivo requires phone numbers in E.164 format. This international standard includes the country code and removes all formatting characters.
E.164 format rules:
+followed by country codeExamples:
+14155552671(US number)+442071838750(UK number)+551155256325(Brazil number)(415) 555-2671(contains formatting)4155552671(missing country code)The validation logic is already included in the service implementation above. The regex
/^\+?[1-9]\d{1,14}$/checks for valid E.164 format.Why E.164 matters: Using the standard format ensures your messages reach the correct recipients across any country. It eliminates ambiguity in phone number interpretation.
For more details, see our complete guide to E.164 phone number format.
Build the Frontend Form
Create a React component for sending SMS campaigns. Generate a new page:
Update
web/src/pages/SendCampaignPage/SendCampaignPage.tsx:// web/src/pages/SendCampaignPage/SendCampaignPage.tsx import { useState } from 'react' import { Toaster, toast } from '@redwoodjs/web/toast' import { Form, Label, TextField, TextAreaField, FieldError, Submit, useForm, } from '@redwoodjs/forms' import { MetaTags, useMutation } from '@redwoodjs/web' // Define the GraphQL Mutation const SEND_PLIVO_MESSAGE_MUTATION = gql` mutation SendPlivoMessageMutation($input: SendPlivoMessageInput!) { sendPlivoMessage(input: $input) { success messageId error } } ` interface FormData { to: string text: string } const SendCampaignPage = () => { const formMethods = useForm<FormData>() const [sendPlivoMessage, { loading, error }] = useMutation( SEND_PLIVO_MESSAGE_MUTATION, { onCompleted: (data) => { if (data.sendPlivoMessage.success) { toast.success( `Message sent successfully! ID: ${data.sendPlivoMessage.messageId}` ) formMethods.reset() } else { toast.error(`Failed to send message: ${data.sendPlivoMessage.error}`) } }, onError: (error) => { toast.error(`GraphQL Error: ${error.message}`) } } ) const onSubmit = (data: FormData) => { console.log('Form data submitted:', data) sendPlivoMessage({ variables: { input: data } }) } return ( <> <MetaTags title="Send Campaign Message" description="Send SMS via Plivo" /> <Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} /> <h1 className="text-2xl font-semibold mb-4">Send Plivo SMS Campaign</h1> <Form<FormData> onSubmit={onSubmit} formMethods={formMethods} className="space-y-4 max-w-md"> <div> <Label name="to" className="block text-sm font-medium text-gray-700"> Recipient Number (E.164 format, e.g., +14155552671) </Label> <TextField name="to" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" validation={{ required: true, pattern: { value: /^\+?[1-9]\d{1,14}$/, message: 'Use E.164 format' } }} /> <FieldError name="to" className="mt-1 text-xs text-red-600" /> </div> <div> <Label name="text" className="block text-sm font-medium text-gray-700"> Message Text </Label> <TextAreaField name="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" validation={{ required: true, minLength: 1 }} /> <FieldError name="text" className="mt-1 text-xs text-red-600" /> </div> {error && <div className="text-red-600">Error: {error.message}</div>} <div> <Submit disabled={loading} className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50" > {loading ? 'Sending...' : 'Send Message'} </Submit> </div> </Form> </> ) } export default SendCampaignPageKey features:
Add Error Handling and Retries
The service implementation above already includes comprehensive error handling with automatic retries using the
async-retrylibrary.Retry configuration explained:
retries: 3– attempts the request up to 3 times before failingfactor: 2– exponential backoff (2x delay between retries)minTimeout: 1000– waits 1 second before first retryWhy retry matters: Network issues or temporary Plivo API failures can cause messages to fail. Implementing retries improves reliability without requiring manual intervention.
When NOT to retry: The current implementation retries all errors. In production, you may want to bail immediately on validation errors (400 status codes) by inspecting the error structure Plivo returns.
Learn more about handling SMS delivery failures and retry strategies.
Implement Authentication
Protect your SMS endpoint from unauthorized access. RedwoodJS provides built-in authentication through the
@requireAuthdirective.Set up authentication:
This creates the authentication infrastructure with database-backed auth (dbAuth).
The
@requireAuthdirective on your mutation (already included in the SDL above) ensures only logged-in users can send SMS:export const schema = gql` type Mutation { sendPlivoMessage(input: SendPlivoMessageInput!): PlivoMessageResult! @requireAuth } `Implement role-based access control (optional):
Restrict SMS sending to specific user roles:
export const schema = gql` type Mutation { sendPlivoMessage(input: SendPlivoMessageInput!): PlivoMessageResult! @requireAuth(roles: ["ADMIN", "MARKETING"]) } `Why this matters: Without authentication, anyone who discovers your API endpoint could send SMS messages using your Plivo account. This leads to unauthorized charges and potential abuse.
For complete authentication setup, see the RedwoodJS authentication documentation.
Test Your SMS Integration
Start your development server:
Access your application:
http://localhost:8910/send-campaign+14155552671)Verify the results:
CampaignLogtable in your database:Look for the new record with status
SENTand the Plivo message UUID.Test error handling:
4155552671without+)FAILEDTest with GraphQL Playground:
Use the GraphQL Playground (
http://localhost:8910/graphql) to test the mutation directly:mutation TestPlivoSend { sendPlivoMessage(input: { to: "+1YOUR_TEST_PHONE_NUMBER", text: "Test message from RedwoodJS GraphQL Playground!" }) { success messageId error } }Deploy to Production
Configure your environment variables on your hosting platform before deploying.
For Vercel:
vercel env add PLIVO_AUTH_ID vercel env add PLIVO_AUTH_TOKEN vercel env add PLIVO_SOURCE_NUMBERFor Netlify:
Add environment variables in the Netlify dashboard:
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN,PLIVO_SOURCE_NUMBERFor AWS/Render/Fly.io:
Consult your platform's documentation for setting environment variables. Each platform has a different method, but all require the same three variables.
Deploy your application:
Post-deployment checklist:
Production recommendations:
Learn more about SMS compliance requirements for production applications.
Frequently Asked Questions (FAQ)
How much does it cost to send SMS with Plivo?
Plivo charges per SMS segment, with pricing varying by destination country. US SMS typically costs $0.0075–$0.01 per segment. Check Plivo's pricing page for current rates in your target countries. A standard SMS segment is 160 characters.
What is E.164 phone number format?
E.164 is the international phone number format required by Plivo. It starts with
+followed by country code and number without spaces or formatting (e.g.,+14155552671for a US number). Maximum 15 digits total. See our complete E.164 format guide.Can I send bulk SMS campaigns with RedwoodJS and Plivo?
Yes. Modify the GraphQL mutation to accept an array of phone numbers and implement batch sending with rate limiting. Consider using a queue system like Bull or Graphile Worker for large campaigns to prevent API throttling. For high-volume sending, you'll also need to register for 10DLC.
How do I track SMS delivery status with Plivo?
Plivo returns a message UUID for each sent SMS. Use this UUID to query the Plivo API for delivery status, or implement webhook handlers to receive real-time delivery notifications when messages are delivered or fail. Check the Plivo delivery reports documentation.
Does RedwoodJS support other SMS providers besides Plivo?
Yes. RedwoodJS works with any SMS provider with a Node.js SDK. Popular alternatives include Twilio, Vonage, MessageBird, and Sinch. The integration pattern remains similar – initialize the client in your service and call the provider's API.
What Node.js version does RedwoodJS require?
RedwoodJS 8.x requires Node.js 20.x or higher. Some deploy targets like AWS Lambda may have restrictions with Node.js 21+. Use the LTS version (20.x) for best compatibility.
How do I handle SMS opt-out requests?
Create a database table to track opt-out requests. Before sending messages, query this table to filter out opted-out numbers. Include an opt-out message in your campaigns (e.g., "Reply STOP to unsubscribe") and implement webhook handlers to process replies. See our SMS compliance guide for TCPA requirements.
Next Steps: Enhance Your SMS Marketing System
You've built a complete SMS marketing campaign system with Plivo and RedwoodJS. Your implementation includes GraphQL mutations, database logging, error handling, and automatic retries.
Advanced features to add:
Bulk SMS campaigns and scheduling:
SMS analytics and reporting:
Compliance and subscriber management:
Integration enhancements:
Related guides:
Additional resources:
Get help and support:
Join the RedwoodJS Discord community for framework questions or consult Plivo's support documentation for API-specific issues. Both communities actively help developers implement SMS solutions.
About this guide: Last updated October 2025 with RedwoodJS 8.x, Plivo Node.js SDK 4.74.0, and Node.js 20.x requirements. Code examples tested on production deployments with Netlify, Vercel, and AWS Lambda.