code examples
code examples
How to Build Bulk SMS Broadcasting with Plivo and RedwoodJS (2025 Guide)
Complete guide to implementing bulk SMS broadcasting in RedwoodJS using Plivo's SMS API. Learn GraphQL setup, batch processing, authentication, webhook integration, and production deployment with working code examples.
Build Bulk SMS Broadcasting with Plivo and RedwoodJS: Complete Implementation Guide
This comprehensive tutorial shows you how to implement a production-ready bulk SMS broadcasting system using RedwoodJS and Plivo's SMS API. Whether you're building emergency alert systems, appointment reminders, promotional campaigns, or time-sensitive notifications, this guide covers everything you need to send SMS messages to hundreds or thousands of recipients simultaneously.
You'll learn how to build a secure GraphQL API that processes bulk SMS requests, implement Plivo's bulk messaging API with proper batching (1,000 recipients per request), add authentication and error handling, track delivery status, and deploy to production. This implementation follows RedwoodJS best practices for services, resolvers, and full-stack development.
Prerequisites and RedwoodJS Project Setup for Bulk SMS
Prerequisites:
- Node.js v22 LTS installed (Active LTS until October 2025)
- Basic familiarity with RedwoodJS, GraphQL, and TypeScript
- Plivo account with active credits (sign up at plivo.com)
- Plivo phone number enabled for SMS (costs vary by country; US numbers typically $1–$3/month)
- Understanding of E.164 phone number format
System requirements: 4 GB RAM minimum, 2 GB free disk space for Node modules and build artifacts.
Plivo pricing consideration: US SMS messages cost approximately $0.0075–$0.01 per message. Test your implementation with small batches before scaling to thousands of recipients.
Create a new RedwoodJS project and install dependencies.
-
Create RedwoodJS App:
bashyarn create redwood-app ./plivo-bulk-senderChoose TypeScript when prompted for better type safety.
-
Navigate to Project Directory:
bashcd plivo-bulk-sender -
Install Plivo Node.js SDK: Install Plivo in the
apiworkspace only (it's server-side code):bashyarn workspace api add plivo @types/plivo-nodeTroubleshooting: If installation fails, verify you're in the project root directory and have Node.js v22 LTS installed. Check your network connection – the Plivo SDK downloads native dependencies.
Version pinning: For production, pin specific versions in
api/package.json(e.g.,"plivo": "4.74.0") to ensure consistent builds across environments. -
Configure Environment Variables: Never hardcode credentials. RedwoodJS uses
.envfiles for secure configuration.Create a
.envfile in the project root:bashtouch .envAdd these variables with your actual Plivo values:
dotenv# .env PLIVO_AUTH_ID="YOUR_PLIVO_AUTH_ID" PLIVO_AUTH_TOKEN="YOUR_PLIVO_AUTH_TOKEN" PLIVO_SOURCE_NUMBER="YOUR_PLIVO_PHONE_NUMBER" # Must be a Plivo number enabled for SMSFind your Plivo credentials:
- Log in to Plivo Console
- Auth ID & Token: Found on the main dashboard page
- Phone Number: Navigate to Messaging → Phone Numbers → Your Numbers. Copy an SMS-enabled number suitable for your target country's regulations.
Verify
.envis in.gitignore: RedwoodJS adds this by default, but confirm your.envfile is listed to prevent committing secrets.Validate configuration: After setting variables, restart your dev server and check logs for Plivo initialization messages. If credentials are invalid, you'll see error messages when attempting to send.
Team/CI environments: For team development, use environment-specific
.env.developmentand.env.testfiles (committed with placeholder values). For CI/CD, configure secrets through your platform's environment variable settings (GitHub Actions secrets, GitLab CI/CD variables, etc.).
How to Implement the Plivo SMS Service in RedwoodJS
RedwoodJS services encapsulate server-side business logic. Create a service to handle Plivo API interactions.
-
Generate Messaging Service:
bashyarn rw g service messagingThis creates
api/src/services/messaging/messaging.tsand test files. -
Implement the Bulk Send Logic: Replace the contents of
api/src/services/messaging/messaging.tswith this implementation:typescript// api/src/services/messaging/messaging.ts import { PlivoClient } from 'plivo-node' import type { MessageCreateResponse } from 'plivo-node/dist/resources/message' import { logger } from 'src/lib/logger' // Initialize Plivo client once at module load for connection reuse // Connection reuse improves performance by avoiding TLS handshake overhead on each request // RedwoodJS automatically loads environment variables from .env let plivoClient: PlivoClient | null = null if (process.env.PLIVO_AUTH_ID && process.env.PLIVO_AUTH_TOKEN) { plivoClient = new PlivoClient( process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN ) } else { logger.error( 'Plivo Auth ID or Auth Token missing in environment variables.' ) // For production, throw an error during startup if Plivo is essential: // throw new Error('Plivo credentials not configured.') } interface SendBulkSmsParams { destinations: string[] text: string } interface BulkSmsResponse { success: boolean message: string plivoResponse?: MessageCreateResponse error?: string } /** * Sends a single SMS message to multiple destinations using Plivo's bulk API format. * * @param destinations - An array of E.164 formatted phone numbers. * @param text - The message body to send. * @returns Promise<BulkSmsResponse> - Indicates success or failure. */ export const sendBulkSms = async ({ destinations, text, }: SendBulkSmsParams): Promise<BulkSmsResponse> => { if (!plivoClient) { logger.error('Plivo client not initialized. Cannot send SMS.') return { success: false, message: 'Plivo client configuration error.', error: 'Plivo client not initialized.', } } if (!destinations || destinations.length === 0) { return { success: false, message: 'No destination numbers provided.' } } if (!text || text.trim() === '') { return { success: false, message: 'Message text cannot be empty.' } } if (!process.env.PLIVO_SOURCE_NUMBER) { logger.error('Plivo source number not configured.') return { success: false, message: 'Source phone number not configured.', error: 'PLIVO_SOURCE_NUMBER missing.', } } // **Crucial: Format destinations for Plivo's bulk API** // Plivo requires destinations as a '<'-delimited string (e.g., '+14155551212<+14155551313') const plivoDestinationString = destinations.join('<') // Plivo enforces a 1,000 recipient limit per API request // Reference: https://www.plivo.com/docs/messaging/api/message/bulk-messaging if (destinations.length > 1000) { logger.warn( `Destination count (${destinations.length}) exceeds Plivo's 1,000 recipient limit. Messages may fail – split into batches of 1,000.` ) } logger.info( `Attempting to send bulk SMS via Plivo to ${destinations.length} numbers.` ) logger.debug(`Formatted destinations: ${plivoDestinationString.substring(0, 50)}…`) try { const response: MessageCreateResponse = await plivoClient.messages.create( process.env.PLIVO_SOURCE_NUMBER, // src: Your Plivo sender number plivoDestinationString, // dst: '<'-delimited recipient string text // text: Message body // Optional: Add status callback URL for delivery receipts // { url: 'https://your-app.com/api/webhooks/plivo-status' } ) logger.info( `Plivo bulk message request successful. API Response: ${response.message}` ) // This response confirms the API request was accepted, not that all messages delivered // Check individual message statuses via Plivo dashboard or delivery webhook callbacks logger.debug(`Plivo response details: ${JSON.stringify(response)}`) return { success: true, message: `Bulk message submitted to Plivo for ${destinations.length} recipients.`, plivoResponse: response, } } catch (error) { logger.error({ error }, 'Plivo API call failed.') // Parse Plivo-specific error responses for detailed feedback let errorMessage = 'Failed to send bulk message via Plivo.' if (error instanceof Error && error.message) { errorMessage += ` Error: ${error.message}` } return { success: false, message: errorMessage, error: error instanceof Error ? error.toString() : JSON.stringify(error), } } }
Key implementation details:
- Import Plivo SDK & Types: Import the necessary client and type definitions for type safety.
- Initialize Client: Create a single Plivo client instance using environment variables. Initializing outside the function enables connection reuse, improving performance by avoiding TLS handshake overhead on every call.
sendBulkSmsFunction:- Validates client initialization, destination numbers, message text, and source number
- Formats Destinations: Plivo's bulk API requires destinations as a '<'-delimited string (e.g.,
'+14155551212<+14155551313<+14155551414'). Usedestinations.join('<'). - API Call: Calls
plivoClient.messages.createwith source number, formatted destination string, and message text - Logging: Uses RedwoodJS's built-in Pino logger for operational visibility
- Error Handling:
try...catchblock captures API errors and returns structured responses - Response: Returns structured object indicating success/failure with raw Plivo response
Design Pattern: This Service Layer pattern isolates Plivo interaction logic from GraphQL resolvers, improving testability and maintainability.
Rate limiting consideration: Plivo enforces account-level rate limits (typically 200–1,000 messages/second depending on your plan). Messages exceeding your rate limit are queued automatically by Plivo, but expect delays for large batches.
Alternative approach: You could loop through destinations and make individual messages.create calls (perhaps using Promise.all). However, this approach is inefficient, hits API rate limits faster, and doesn't leverage Plivo's bulk optimization. The '<'-delimited string is Plivo's recommended method for bulk sending.
How to Create the GraphQL API for Bulk SMS Broadcasting
RedwoodJS uses GraphQL for its API. Define a mutation to expose the sendBulkSms service function.
-
Define GraphQL Schema: Open or create
api/src/graphql/messaging.sdl.tsand add:graphql// api/src/graphql/messaging.sdl.ts export const schema = gql` """ Response type for the sendBulkMessage mutation. """ type BulkSmsResponse { success: Boolean! message: String! # Optional: Expose Plivo response details if needed by the client # plivoMessageUUIDs: [String!] error: String } type Mutation { """ Sends a bulk SMS message to multiple recipients via Plivo. Requires authentication. """ sendBulkMessage(destinations: [String!]!, text: String!): BulkSmsResponse! @requireAuth # Secure this endpoint # Add roles if needed: @requireAuth(roles: ["ADMIN", "EDITOR"]) } `Schema details:
BulkSmsResponseType: Defines the mutation response structure. GraphQL's type system provides automatic validation and clear client contracts.MutationType: DefinessendBulkMessagemutation with:destinationsargument: Array of non-null strings (enforced by GraphQL)textargument: Non-null string- Returns non-null
BulkSmsResponse @requireAuth: RedwoodJS directive ensuring only authenticated users call this mutation. Add role-based access control if needed.
-
Resolver Mapping (Automatic): RedwoodJS automatically maps the
sendBulkMessagemutation to thesendBulkSmsfunction inapi/src/services/messaging/messaging.ts. No explicit resolver code needed. -
Testing the Endpoint: Start your RedwoodJS app:
bashyarn rw devTest using GraphQL Playground at
http://localhost:8911/graphqlor curl.GraphQL Playground test (recommended): Navigate to
http://localhost:8911/graphqland use this mutation:graphqlmutation SendBulk($dest: [String!]!, $msg: String!) { sendBulkMessage(destinations: $dest, text: $msg) { success message error } }Variables:
json{ "dest": ["+14155551212", "+14155551313"], "msg": "Hello from RedwoodJS Bulk Sender!" }Curl Example: (Requires authentication setup first. For initial testing without auth, temporarily remove
@requireAuthfrom schema)bashcurl 'http://localhost:8910/graphql' \ -H 'Content-Type: application/json' \ --data-raw '{"query":"mutation SendBulk($dest: [String!]!, $msg: String!) {\n sendBulkMessage(destinations: $dest, text: $msg) {\n success\n message\n error\n }\n}","variables":{"dest":["+14155551212", "+14155551313"], "msg":"Hello from RedwoodJS!"}}' \ --compressedExpected Success Response:
json{ "data": { "sendBulkMessage": { "success": true, "message": "Bulk message submitted to Plivo for 2 recipients.", "error": null } } }Expected Error Response (e.g., invalid number):
json{ "data": { "sendBulkMessage": { "success": false, "message": "Failed to send bulk message via Plivo. Error: Invalid 'dst' parameter", "error": "Error: Invalid 'dst' parameter" } } }
Plivo SMS API Integration Best Practices for RedwoodJS
The core integration is complete in messaging.ts. This section covers additional integration considerations.
Configuration Summary
- API Credentials:
PLIVO_AUTH_IDandPLIVO_AUTH_TOKENstored in.env, accessed viaprocess.env - Source Number:
PLIVO_SOURCE_NUMBERstored in.env, used as sender number - Credential Management: Manage through Plivo Console
Secure Secrets Management
Development:
- Never commit
.envto version control – verify it's in.gitignore - Use
.env.examplewith placeholder values for team collaboration
Production/Deployment:
- Use your hosting provider's environment variable configuration:
- Vercel: Environment Variables section
- Netlify: Build Environment Variables
- Render: Secret Files or Environment Variables
- AWS: Systems Manager Parameter Store or Secrets Manager
- Never include
.envin deployment bundles
Webhook Integration for Delivery Receipts
To track individual message delivery status, configure Plivo webhooks:
-
Add a webhook URL to your message creation call:
typescriptconst response = await plivoClient.messages.create( process.env.PLIVO_SOURCE_NUMBER, plivoDestinationString, text, { url: 'https://your-app.com/api/webhooks/plivo-status' } ) -
Create a webhook handler in RedwoodJS:
typescript// api/src/functions/plivoWebhook/plivoWebhook.ts export const handler = async (event) => { const body = JSON.parse(event.body) // Process delivery status: body.Status, body.MessageUUID, etc. logger.info(`Message ${body.MessageUUID} status: ${body.Status}`) return { statusCode: 200, body: 'OK' } } -
Configure the webhook URL in Plivo Console under Messaging → Your Application → Message URL
Fallback Mechanisms
| Scenario | Solution | Complexity |
|---|---|---|
| Temporary Plivo outage | Implement retry with exponential backoff (covered in next section) | Medium |
| Extended Plivo outage | Message queue system (BullMQ + Redis) for async processing | High |
| Critical system requiring 99.99% uptime | Secondary SMS provider with automatic failover | Very High |
For most applications, retry logic with exponential backoff provides sufficient resilience without added complexity.
Error Handling and Retry Strategies for Bulk SMS Delivery
Robust error handling and logging are essential for production systems.
Error Handling Strategy
Service Level:
try...catchblocks capture Plivo API errors- Returns structured
BulkSmsResponsewithsuccess: falseand error details - Logs all errors with context for debugging
GraphQL Level:
- Service-level error handling provides controlled failure responses
- GraphQL errors (if service throws) propagate per GraphQL specification
- Client receives consistent response format
Common Plivo Error Scenarios:
| Error Type | Cause | Solution |
|---|---|---|
Authentication failed | Invalid AUTH_ID or AUTH_TOKEN | Verify credentials in Plivo Console |
Invalid 'dst' parameter | Malformed phone number or non-E.164 format | Validate numbers with libphonenumber-js |
Insufficient balance | Account out of credits | Add credits in Plivo Console |
Rate limit exceeded | Too many requests per second | Implement request throttling or increase rate limit |
Invalid 'src' parameter | Source number not owned or not SMS-enabled | Verify number in Plivo Console under Phone Numbers |
Logging Configuration
Log Levels:
logger.info: Successful operations, key events (message submitted)logger.error: Failures, exceptions (API errors)logger.debug: Detailed information (API responses, formatted numbers)logger.warn: Potential issues (recipient count exceeding limits)
Configure per environment:
// api/src/lib/logger.ts
export const logger = createLogger({
options: {
level: process.env.LOG_LEVEL || 'info', // 'debug' for dev, 'info' for prod
},
})Production log forwarding: Forward logs to aggregation services for analysis and alerting:
- Datadog: Use
pino-datadogtransport - Logtail: Use Logtail's Pino integration
- Papertrail: Use
pino-papertrail
Example configuration:
# .env (production)
LOG_LEVEL=info
LOGTAIL_SOURCE_TOKEN=your_logtail_tokenRetry Mechanisms
Simple Retry with Exponential Backoff:
For transient network errors, implement retry logic:
// Example implementation (add to sendBulkSms function)
const maxAttempts = 3
let attempt = 0
let lastError: Error | null = null
while (attempt < maxAttempts) {
attempt++
try {
const response = await plivoClient.messages.create(...)
logger.info(`Message sent successfully on attempt ${attempt}`)
return { success: true, message: '...', plivoResponse: response }
} catch (error) {
lastError = error as Error
// Only retry on network/server errors (5xx), not client errors (4xx)
const isRetryable = error.message?.includes('ECONNRESET') ||
error.message?.includes('ETIMEDOUT')
if (!isRetryable || attempt >= maxAttempts) {
break
}
const delayMs = Math.pow(2, attempt) * 1000 // 2s, 4s, 8s...
logger.warn(`Retrying Plivo API call (attempt ${attempt}/${maxAttempts}) after ${delayMs}ms`)
await new Promise(resolve => setTimeout(resolve, delayMs))
}
}
return {
success: false,
message: `Failed after ${attempt} attempts: ${lastError?.message}`,
error: lastError?.toString()
}Background Jobs (Recommended for Scale):
For production systems handling large volumes, decouple SMS sending using background jobs:
Benefits:
- Prevents GraphQL request timeouts
- Enables automatic retries independent of user requests
- Processes large batches without blocking
- Provides job status tracking
Implementation options:
| Solution | Use Case | Setup Complexity |
|---|---|---|
| RedwoodJS Jobs (experimental) | Simple use cases, integrated with RedwoodJS | Low |
| BullMQ + Redis | Production-scale, robust retry/failure handling | Medium |
| AWS SQS + Lambda | AWS deployments, serverless architecture | High |
BullMQ example structure:
// api/src/jobs/sendBulkSms.ts
import { Queue, Worker } from 'bullmq'
const smsQueue = new Queue('bulk-sms', { connection: redisConnection })
// Enqueue job from GraphQL mutation
export const enqueueBulkSms = async (destinations: string[], text: string) => {
await smsQueue.add('send', { destinations, text }, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
})
}
// Worker processes jobs
const worker = new Worker('bulk-sms', async (job) => {
const { destinations, text } = job.data
return await sendBulkSms({ destinations, text })
}, { connection: redisConnection })How to Track SMS Delivery with Database Logging (Optional)
Database logging provides an audit trail for compliance, cost tracking, and debugging.
Privacy and Compliance Considerations
GDPR/Privacy compliance:
- Store only necessary data (recipient count, message status – not full phone numbers unless required)
- Implement data retention policies (auto-delete records after 90 days)
- Include privacy notice in your terms of service
- Provide user data export/deletion capabilities
Retention policy example:
// api/src/services/cleanup/cleanup.ts
export const cleanupOldMessageJobs = async () => {
const ninetyDaysAgo = new Date()
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)
const deleted = await db.bulkMessageJob.deleteMany({
where: { createdAt: { lt: ninetyDaysAgo } }
})
logger.info(`Deleted ${deleted.count} message job records older than 90 days`)
}Define Prisma Schema
Open api/db/schema.prisma and add a model for logging:
// api/db/schema.prisma
datasource db {
provider = "postgresql" // Or sqlite, mysql
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model BulkMessageJob {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status String // "PENDING", "SUBMITTED", "FAILED"
messageText String
recipientCount Int
plivoMessageId String? // MessageUUID from Plivo (correlates with logs)
errorMessage String?
@@index([status, createdAt]) // Query optimization for status filters
@@index([createdAt]) // Supports retention cleanup queries
}Index strategy:
[status, createdAt]: Optimizes queries filtering by status with date ranges[createdAt]: Supports efficient retention policy cleanup
Apply Migrations
Generate and apply the migration:
yarn rw prisma migrate dev --name add_bulk_message_jobUpdate Service to Log
Modify api/src/services/messaging/messaging.ts to write database logs:
// api/src/services/messaging/messaging.ts
import { db } from 'src/lib/db' // RedwoodJS Prisma client
import { PlivoClient } from 'plivo-node'
import type { MessageCreateResponse } from 'plivo-node/dist/resources/message'
import { logger } from 'src/lib/logger'
// ... (Plivo client initialization)
export const sendBulkSms = async ({
destinations,
text,
}: SendBulkSmsParams): Promise<BulkSmsResponse> => {
// ... (validation checks)
const plivoDestinationString = destinations.join('<')
let jobRecordId: string | null = null
try {
// Create pending job record
const jobRecord = await db.bulkMessageJob.create({
data: {
status: 'PENDING',
messageText: text,
recipientCount: destinations.length,
},
select: { id: true },
})
jobRecordId = jobRecord.id
logger.info(
`Attempting bulk SMS (Job ID: ${jobRecordId}) to ${destinations.length} recipients`
)
if (!plivoClient || !process.env.PLIVO_SOURCE_NUMBER) {
throw new Error('Plivo client or source number not configured.')
}
const response: MessageCreateResponse = await plivoClient.messages.create(
process.env.PLIVO_SOURCE_NUMBER,
plivoDestinationString,
text
)
logger.info(
`Bulk SMS successful (Job ID: ${jobRecordId}). API Response: ${response.message}`
)
// Update job record on success
await db.bulkMessageJob.update({
where: { id: jobRecordId },
data: {
status: 'SUBMITTED',
plivoMessageId: response.messageUuid?.[0],
updatedAt: new Date(),
},
})
return {
success: true,
message: `Bulk message submitted to Plivo for ${destinations.length} recipients.`,
plivoResponse: response,
}
} catch (error) {
logger.error({ error, jobRecordId }, 'Plivo API call failed.')
const errorMessage =
error instanceof Error ? error.toString() : JSON.stringify(error)
// Update job record on failure
if (jobRecordId) {
try {
await db.bulkMessageJob.update({
where: { id: jobRecordId },
data: {
status: 'FAILED',
errorMessage: errorMessage.substring(0, 1000), // Truncate long errors
updatedAt: new Date(),
},
})
} catch (dbError) {
logger.error(
{ error: dbError, originalJobId: jobRecordId },
'Failed to update job status to FAILED.'
)
}
}
return {
success: false,
message: `Failed to send bulk message via Plivo. Error: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
error: errorMessage,
}
}
}Performance considerations:
- For high volume (>100 messages/second), database writes become bottlenecks
- Consider async logging via background jobs
- Batch multiple log writes using Prisma transactions
- Monitor database connection pool utilization
How to Secure Your Bulk SMS API with RedwoodJS Authentication
Security is critical – bulk SMS endpoints can incur costs and enable abuse if exposed.
Authentication Setup
The @requireAuth directive in your GraphQL schema provides the first security layer. Configure authentication:
Setup dbAuth (self-hosted):
yarn rw setup auth dbAuthThis scaffolds:
- Login/signup pages
- Session handling with secure HTTP-only cookies
- Password hashing with bcrypt
- User model in Prisma schema
Alternative providers:
- Auth0: Enterprise-grade authentication
- Netlify Identity: Simple integration for Netlify deployments
- Firebase Auth: Google-backed authentication with social providers
- Clerk: Modern authentication with React components
Follow RedwoodJS Authentication docs for setup instructions.
Verify authentication works:
- Start dev server:
yarn rw dev - Navigate to
/login(scaffolded by dbAuth) - Create a test account
- Attempt to call
sendBulkMessagemutation – should succeed when authenticated, fail when logged out
Authorization (Role-Based Access Control)
Restrict bulk messaging to specific user roles:
// api/src/graphql/messaging.sdl.ts
type Mutation {
sendBulkMessage(destinations: [String!]!, text: String!): BulkSmsResponse!
@requireAuth(roles: ["admin", "manager"])
}Implement roles in User model:
// api/db/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
hashedPassword String
salt String
roles String @default("user") // "user", "admin", "manager"
// ... other fields
}Rate Limiting
Prevent abuse by implementing rate limiting:
Install rate limiting library:
yarn workspace api add express-rate-limitConfigure rate limiter:
// api/src/functions/graphql.ts
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
})
// Apply to GraphQL endpoint
export const handler = createGraphQLHandler({
// ... other config
extraPlugins: [limiter],
})Input Validation and Sanitization
GraphQL type enforcement:
The schema enforces destinations as non-null string array and text as non-null string.
Phone number validation with libphonenumber-js:
Install library:
yarn workspace api add libphonenumber-jsAdd validation to service:
import { parsePhoneNumber } from 'libphonenumber-js'
export const sendBulkSms = async ({
destinations,
text,
}: SendBulkSmsParams): Promise<BulkSmsResponse> => {
// Validate and normalize phone numbers
const validatedNumbers: string[] = []
const invalidNumbers: string[] = []
for (const number of destinations) {
try {
const parsed = parsePhoneNumber(number)
if (parsed && parsed.isValid()) {
validatedNumbers.push(parsed.format('E.164'))
} else {
invalidNumbers.push(number)
}
} catch {
invalidNumbers.push(number)
}
}
if (invalidNumbers.length > 0) {
return {
success: false,
message: `Invalid phone numbers: ${invalidNumbers.join(', ')}`,
}
}
// Continue with validatedNumbers...
}Message length validation:
const MAX_SMS_LENGTH = 1600 // GSM-7 max with concatenation
if (text.length > MAX_SMS_LENGTH) {
return {
success: false,
message: `Message exceeds maximum length of ${MAX_SMS_LENGTH} characters.`,
}
}Frequently Asked Questions About Plivo Bulk SMS with RedwoodJS
How many recipients can I send to in a single Plivo API request?
Plivo supports up to 1,000 unique destination numbers per API request for bulk messaging. For larger lists, split them into batches of 1,000 or fewer. Messages may not deliver instantly – they're queued based on your account's rate limits.
Reference: https://www.plivo.com/docs/messaging/api/message/bulk-messaging
What Node.js version should I use for RedwoodJS and Plivo integration?
Use Node.js v22 LTS for production. Node.js v22 remains in Active LTS until October 2025, then transitions to Maintenance LTS until April 2027. Plivo's Node.js SDK (v4.74.0) supports Node.js 5.5 and higher, making v22 LTS ideal for long-term stability.
How do I format phone numbers for Plivo's bulk SMS API?
Plivo requires E.164 format (e.g., +14155551212). For bulk messaging, separate multiple numbers with the < character: +14155551212<+14155551313<+14155551414. Use libphonenumber-js to validate and normalize numbers before sending.
What are the SMS character limits for Plivo messages?
Character limits depend on encoding:
| Encoding | Single Message | Per Segment (Concatenated) | Max Total |
|---|---|---|---|
| GSM-7 | 160 characters | 153 characters | 1,600 characters |
| UCS-2 | 70 characters | 67 characters | 737 characters |
Plivo automatically detects encoding. GSM-7 covers standard English/Western European characters. UCS-2 handles Unicode (emojis, special characters). Plivo's intelligent encoding replaces common Unicode characters (smart quotes, em dashes) with GSM-7 equivalents to optimize costs.
Reference: https://www.plivo.com/docs/messaging/concepts/encoding-and-concatenation
How do I secure my bulk SMS endpoint in RedwoodJS?
Use RedwoodJS's @requireAuth directive in your GraphQL schema. For role-based access, use @requireAuth(roles: ["admin", "manager"]). Configure authentication with dbAuth (self-hosted) or third-party providers (Auth0, Firebase Auth, Netlify Identity). Implement rate limiting to prevent abuse. Never expose bulk SMS endpoints without authentication.
What is the best way to handle Plivo API errors in RedwoodJS?
Implement comprehensive error handling with try...catch blocks in your service layer. Log errors using RedwoodJS's Pino logger (logger.error). For transient network errors, implement exponential backoff retry logic (wait 1s, 2s, 4s…). For production, use background job queues (RedwoodJS Jobs or BullMQ + Redis) to decouple sending from GraphQL requests, preventing timeouts and ensuring delivery during temporary outages.
How do I test my bulk SMS implementation without sending real messages?
Development testing approaches:
-
Plivo test credentials: Plivo provides test credentials that simulate API calls without sending real messages or charging your account. Find these in your Plivo Console under Account Settings → Test Credentials.
-
Mock testing: For automated tests, mock the Plivo client using Jest:
typescript// messaging.test.ts jest.mock('plivo-node') const mockCreate = jest.fn().mockResolvedValue({ message: 'message(s) queued', messageUuid: ['test-uuid-123'] }) PlivoClient.mockImplementation(() => ({ messages: { create: mockCreate } })) -
GraphQL Playground: Test at
http://localhost:8911/graphqlwith sample data (use your own number to verify delivery) -
Temporary auth bypass: Remove
@requireAuthduring initial testing (remember to add back before deployment)
Should I log SMS messages to a database in production?
Database logging is optional but recommended for production:
Benefits:
- Audit trail for compliance
- Diagnose delivery issues
- Track costs
- Monitor usage patterns
Implementation:
Create a BulkMessageJob model storing status (PENDING, SUBMITTED, FAILED), recipient count, Plivo message UUIDs, and error messages.
Considerations:
- Implement data retention policies (GDPR compliance)
- For high volume (>100 messages/second), use async logging or batch writes
- Monitor database performance impact
- Consider privacy implications of storing phone numbers
How do I deploy a RedwoodJS bulk SMS application to production?
Deployment platforms:
| Platform | Best For | Setup Complexity |
|---|---|---|
| Vercel | Next.js-style deployments, simple setup | Low |
| Netlify | Integrated CI/CD, instant rollbacks | Low |
| Render | Full-stack apps, managed PostgreSQL | Medium |
| AWS (Amplify/Elastic Beanstalk) | Enterprise, full control | High |
Deployment checklist:
-
Environment variables: Configure
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN,PLIVO_SOURCE_NUMBERthrough your platform's settings – never commit.env -
Database migrations:
bashyarn rw prisma migrate deploy -
Log forwarding: Configure production logging (Datadog, Logtail)
-
Rate limiting: Implement request throttling for API protection
-
Message queues: For high volume, deploy BullMQ with Redis or use AWS SQS
-
Monitoring: Set up alerts for error rates, API costs, delivery failures
-
CI/CD: Configure GitHub Actions or GitLab CI for automated testing and deployment
Example GitHub Actions workflow:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '22'
- run: yarn install
- run: yarn rw test
- run: yarn rw prisma migrate deploy
- run: yarn rw deploy netlify
env:
PLIVO_AUTH_ID: ${{ secrets.PLIVO_AUTH_ID }}
PLIVO_AUTH_TOKEN: ${{ secrets.PLIVO_AUTH_TOKEN }}What are the cost considerations for bulk SMS with Plivo?
Pricing structure:
- Plivo uses pay-as-you-go pricing varying by destination country
- US SMS: $0.0075–$0.01 per message
- International rates vary significantly (check Plivo pricing page)
- No per-message discount for bulk sending (same rate as individual messages)
Cost optimization strategies:
- Character optimization: Use Plivo's automatic encoding to minimize segments
- Phone number validation: Avoid wasted messages to invalid numbers
- Message batching: Reduce API overhead (not message costs)
- Monitor usage: Track daily/monthly spend in Plivo Console
- Volume pricing: Contact Plivo for discounts on high volumes (100,000+ messages/month)
Example cost calculation:
- 10,000 recipients
- US destinations
- Average rate: $0.0085/message
- Total: 10,000 × $0.0085 = $85
Budget alerts: Set up spending notifications in Plivo Console to prevent unexpected bills.
Frequently Asked Questions
How to send bulk SMS with RedwoodJS?
You can send bulk SMS messages by creating a RedwoodJS service that interacts with the Plivo API. This service will handle formatting destination numbers and sending the message text via a GraphQL mutation secured with `@requireAuth`.
What is Plivo used for in RedwoodJS?
Plivo is a cloud communications platform that provides SMS, voice, and WhatsApp APIs, used in this RedwoodJS application for its robust messaging capabilities and support for bulk sending. It allows you to send messages to multiple recipients with a single API request, reducing latency and simplifying logic.
Why use RedwoodJS for bulk SMS?
RedwoodJS offers an integrated full-stack structure, uses GraphQL, and provides developer-friendly features like generators and cells. Its backend services neatly encapsulate business logic like Plivo integration, offering a streamlined development experience for building a bulk SMS feature.
What is the system architecture for bulk SMS sending?
The architecture involves a web frontend sending requests to a RedwoodJS GraphQL API. The API interacts with a RedwoodJS Plivo service, which formats and sends messages via the Plivo API. Optionally, a RedwoodJS database logs messages and handles user interactions.
How to set up a RedwoodJS project for bulk SMS?
Create a new RedwoodJS project, install the Plivo Node.js SDK and types in the API workspace, and configure environment variables (`PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`, `PLIVO_SOURCE_NUMBER`) in a `.env` file. These credentials are essential for interacting with the Plivo API.
Where do I find my Plivo Auth ID and Auth Token?
Your Plivo Auth ID and Auth Token can be found on the main dashboard page after logging in to your Plivo Console at https://console.plivo.com/. Plivo documentation provides visual guides to their location on the dashboard if needed.
Where can I find my Plivo phone number for sending SMS?
Your Plivo phone number can be found in the Plivo Console by navigating to Messaging -> Phone Numbers -> Your Numbers. Be sure to select a number enabled for SMS and compliant with country regulations.
What format does Plivo expect for bulk destinations?
Plivo expects destination phone numbers to be concatenated into a single string delimited by the '<' character. For instance, "14155551212<14155551313<14155551414". This allows sending to multiple recipients in one API request.
How to secure the RedwoodJS bulk SMS API endpoint?
Secure the endpoint using `@requireAuth` directive in the GraphQL schema and set up an authentication provider (like dbAuth, Auth0, Netlify Identity, etc.). Restrict access further using roles (e.g., "admin", "manager") if needed.
How to validate phone numbers for Plivo?
While Plivo handles some number variations, validate and normalize numbers into E.164 format (e.g., +14155551212) before sending them to the service to prevent errors. Libraries like `libphonenumber-js` are helpful for this purpose.
What are the error handling mechanisms for Plivo API calls?
The `sendBulkSms` service function uses try-catch blocks to capture and log errors during Plivo API calls. The function returns a structured response indicating success or failure with error messages. For production, consider more robust retry mechanisms or queueing systems.
How to implement error logging for the bulk SMS feature?
Utilize Redwood's built-in Pino logger (`src/lib/logger.ts`) to log successful operations (info), failures (error), and detailed information (debug). Configure log levels per environment and consider forwarding logs to dedicated logging services in production.
Why does this example initialize the Plivo client outside the service function?
Initializing the Plivo client outside the `sendBulkSms` function allows for potential reuse and avoids recreating the client instance on every call, improving efficiency. It also ensures that environment variables needed for the Plivo client are properly loaded.
When should I use background jobs for sending bulk SMS?
Background jobs are recommended for production-level bulk SMS sending, especially for large broadcasts or potential Plivo delays. This decouples sending from the initial request and improves reliability by handling retries and failures independently.
Can I log bulk message attempts to a database?
Yes, the example provides an optional logging mechanism using Prisma. A `BulkMessageJob` model is added to the Prisma schema, allowing you to store job status, recipient count, messages, errors, and potentially destinations for tracking and analysis.