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.
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 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:
Bash
yarn create redwood-app ./plivo-bulk-sender
Choose TypeScript when prompted for better type safety.
Navigate to Project Directory:
Bash
cd plivo-bulk-sender
Install Plivo Node.js SDK:
Install Plivo in the api workspace only (it's server-side code):
Bash
yarn workspace api add plivo @types/plivo-node
Troubleshooting: 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 .env files for secure configuration.
Create a .env file in the project root:
Bash
touch .env
Add these variables with your actual Plivo values:
.env
# .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 SMS
Phone Number: Navigate to Messaging → Phone Numbers → Your Numbers. Copy an SMS-enabled number suitable for your target country's regulations.
Verify .env is in .gitignore: RedwoodJS adds this by default, but confirm your .env file 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.development and .env.test files (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:
Bash
yarn rw g service messaging
This creates api/src/services/messaging/messaging.ts and test files.
Implement the Bulk Send Logic:
Replace the contents of api/src/services/messaging/messaging.ts with this implementation:
TypeScript
// api/src/services/messaging/messaging.tsimport { PlivoClient } from'plivo-node'importtype { 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 .envletplivoClient: PlivoClient | null = nullif (process.env.PLIVO_AUTH_ID && process.env.PLIVO_AUTH_TOKEN) {
plivoClient = newPlivoClient(
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.')
}
interfaceSendBulkSmsParams {
destinations: string[]
text: string
}
interfaceBulkSmsResponse {
success: booleanmessage: stringplivoResponse?: MessageCreateResponseerror?: string
}
/**
* Sends a single SMS message to multiple destinations using Plivo's bulk API format.
*
* @paramdestinations - An array of E.164 formatted phone numbers.
* @paramtext - The message body to send.
* @returns Promise<BulkSmsResponse> - Indicates success or failure.
*/exportconst 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-messagingif (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 {
constresponse: 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 feedbacklet errorMessage = 'Failed to send bulk message via Plivo.'if (error instanceofError && error.message) {
errorMessage += ` Error: ${error.message}`
}
return {
success: false,
message: errorMessage,
error: error instanceofError ? 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.
sendBulkSms Function:
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'). Use destinations.join('<').
API Call: Calls plivoClient.messages.create with source number, formatted destination string, and message text
Logging: Uses RedwoodJS's built-in Pino logger for operational visibility
Error Handling:try...catch block 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.ts and 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
}typeMutation{"""
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:
BulkSmsResponse Type: Defines the mutation response structure. GraphQL's type system provides automatic validation and clear client contracts.
destinations argument: Array of non-null strings (enforced by GraphQL)
text argument: 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 sendBulkMessage mutation to the sendBulkSms function in api/src/services/messaging/messaging.ts. No explicit resolver code needed.
Testing the Endpoint:
Start your RedwoodJS app:
Bash
yarn rw dev
Test using GraphQL Playground at http://localhost:8911/graphql or curl.
GraphQL Playground test (recommended):
Navigate to http://localhost:8911/graphql and use this mutation:
constMAX_SMS_LENGTH = 1600// GSM-7 max with concatenationif (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.
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.
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:
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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:
Choose TypeScript when prompted for better type safety.
Navigate to Project Directory:
cd plivo-bulk-senderInstall Plivo Node.js SDK: Install Plivo in the
apiworkspace only (it's server-side code):Troubleshooting: 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:touch .envAdd these variables with your actual Plivo values:
Find your Plivo credentials:
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:
This 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:// 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:
sendBulkSmsFunction:'+14155551212<+14155551313<+14155551414'). Usedestinations.join('<').plivoClient.messages.createwith source number, formatted destination string, and message texttry...catchblock captures API errors and returns structured responsesDesign 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.createcalls (perhaps usingPromise.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
sendBulkSmsservice function.Define GraphQL Schema: Open or create
api/src/graphql/messaging.sdl.tsand add:// 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 stringBulkSmsResponse@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:
Test using GraphQL Playground at
http://localhost:8911/graphqlor curl.GraphQL Playground test (recommended): Navigate to
http://localhost:8911/graphqland use this mutation:mutation SendBulk($dest: [String!]!, $msg: String!) { sendBulkMessage(destinations: $dest, text: $msg) { success message error } }Variables:
{ "dest": ["+14155551212", "+14155551313"], "msg": "Hello from RedwoodJS Bulk Sender!" }Curl Example: (Requires authentication setup first. For initial testing without auth, temporarily remove
@requireAuthfrom schema)curl '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:
{ "data": { "sendBulkMessage": { "success": true, "message": "Bulk message submitted to Plivo for 2 recipients.", "error": null } } }Expected Error Response (e.g., invalid number):
{ "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
PLIVO_AUTH_IDandPLIVO_AUTH_TOKENstored in.env, accessed viaprocess.envPLIVO_SOURCE_NUMBERstored in.env, used as sender numberSecure Secrets Management
Development:
.envto version control – verify it's in.gitignore.env.examplewith placeholder values for team collaborationProduction/Deployment:
.envin deployment bundlesWebhook Integration for Delivery Receipts
To track individual message delivery status, configure Plivo webhooks:
Add a webhook URL to your message creation call:
const 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:
// 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
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 errorsBulkSmsResponsewithsuccess: falseand error detailsGraphQL Level:
Common Plivo Error Scenarios:
Authentication failedInvalid 'dst' parameterlibphonenumber-jsInsufficient balanceRate limit exceededInvalid 'src' parameterLogging 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:
pino-datadogtransportpino-papertrailExample 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:
Implementation options:
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:
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.prismaand add a model for logging:Index strategy:
[status, createdAt]: Optimizes queries filtering by status with date ranges[createdAt]: Supports efficient retention policy cleanupApply Migrations
Generate and apply the migration:
Update Service to Log
Modify
api/src/services/messaging/messaging.tsto 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:
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
@requireAuthdirective in your GraphQL schema provides the first security layer. Configure authentication:Setup dbAuth (self-hosted):
This scaffolds:
Alternative providers:
Follow RedwoodJS Authentication docs for setup instructions.
Verify authentication works:
yarn rw dev/login(scaffolded by dbAuth)sendBulkMessagemutation – should succeed when authenticated, fail when logged outAuthorization (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:
Rate Limiting
Prevent abuse by implementing rate limiting:
Install rate limiting library:
Configure 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
destinationsas non-null string array andtextas non-null string.Phone number validation with libphonenumber-js:
Install library:
Add 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. Uselibphonenumber-jsto validate and normalize numbers before sending.What are the SMS character limits for Plivo messages?
Character limits depend on encoding:
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
@requireAuthdirective 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...catchblocks 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:
// 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:
Implementation: Create a
BulkMessageJobmodel storing status (PENDING,SUBMITTED,FAILED), recipient count, Plivo message UUIDs, and error messages.Considerations:
How do I deploy a RedwoodJS bulk SMS application to production?
Deployment platforms:
Deployment checklist:
Environment variables: Configure
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN,PLIVO_SOURCE_NUMBERthrough your platform's settings – never commit.envDatabase migrations:
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:
Cost optimization strategies:
Example cost calculation:
Budget alerts: Set up spending notifications in Plivo Console to prevent unexpected bills.