code examples
code examples
How to Build SMS Marketing Campaigns with Plivo and RedwoodJS [2025 Guide]
Complete RedwoodJS + Plivo SMS tutorial: Build automated marketing campaigns with GraphQL, track delivery with Prisma, implement retry logic, and deploy to production. 2-3 hour implementation.
Build SMS Marketing Campaigns with Plivo and RedwoodJS
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)
- Plivo account with API credentials
Table of Contents
- Prerequisites for Plivo SMS Integration
- Set Up Your RedwoodJS Project
- Configure Your Environment Variables
- Create the Database Schema
- Define Your GraphQL Schema
- Implement the SMS Service
- Validate Phone Numbers (E.164 Format)
- Build the Frontend Form
- Add Error Handling and Retries
- Implement Authentication
- Test Your SMS Integration
- Deploy to Production
- Frequently Asked Questions
- Next Steps
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)
- Yarn (>=1.15)
- A Plivo account (Sign up at https://www.plivo.com/)
- You'll need your Auth ID and Auth Token.
- 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:
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):
yarn workspace api add plivoThe Plivo SDK package name is plivo on npm – this is the official, maintained version of the SDK.
Related guides:
- Learn about phone number validation and E.164 format
- Compare SMS API providers and pricing
- Explore RedwoodJS GraphQL best practices
- Understand SMS marketing campaign best practices
- Set up 10DLC registration for US SMS campaigns
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:text# .env PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_SOURCE_NUMBER=YOUR_PLIVO_PHONE_NUMBER -
How to find Plivo Credentials:
- Log in to your Plivo console (https://console.plivo.com/).
- The Auth ID and Auth Token are displayed prominently on the main dashboard overview page.
- Navigate to
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.text# .gitignore # ... other entries .env # ... -
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:
yarn redwood devThis 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:
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:
yarn rw prisma migrate dev --name create_campaign_log-
Entity Relationship Diagram (ERD): For this simple case, we only have one model.
mermaiderDiagram 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 }
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:
yarn rw g sdl PlivoCampaign --empty
yarn rw g service PlivoCampaign --emptyThis creates:
-
api/src/graphql/plivoCampaign.sdl.ts -
api/src/services/plivoCampaign/plivoCampaign.ts -
api/src/services/plivoCampaign/plivoCampaign.test.ts -
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@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:
yarn workspace api add async-retry @types/async-retryHow 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.
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:
yarn rw g page SendCampaign /send-campaignUpdate 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:
- 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 failingfactor: 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.
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 @requireAuth directive.
Set up authentication:
yarn rw setup auth dbAuthThis creates the authentication infrastructure with database-backed auth (dbAuth).
The @requireAuth directive 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:
yarn rw devAccess your application:
- Open your browser to
http://localhost:8910/send-campaign - Enter a phone number in E.164 format (e.g.,
+14155552671) - Type a test message
- Click "Send Message"
Verify the results:
- Check for a success toast notification with the message UUID
- Confirm the SMS arrives at the destination phone
- Inspect the
CampaignLogtable in your database:
yarn rw prisma studioLook for the new record with status SENT and the Plivo message UUID.
Test error handling:
- Try an invalid phone number format (e.g.,
4155552671without+) - Verify you see an error message
- Check the database for a record with status
FAILED
Test 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:
- 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:
yarn rw deployPost-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
- Set up webhooks for real-time delivery updates
- Implement rate limiting to prevent abuse
- Monitor SMS costs and set billing alerts
- Review logs regularly for failed deliveries
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., +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)
Related guides:
- Twilio vs Plivo: SMS API comparison
- 10DLC SMS registration for US campaigns
- Building two-way SMS conversations
- SMS compliance guide: TCPA and GDPR
- SMS marketing best practices for 2025
Additional resources:
- Plivo SMS API documentation – Official Plivo API reference
- RedwoodJS GraphQL guide – GraphQL best practices in RedwoodJS
- Prisma schema reference – Complete Prisma documentation
- E.164 phone number format specification – International telecommunication standard
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.