code examples
code examples
Send SMS with Plivo, Node.js & RedwoodJS: Complete Integration Guide
Integrate Plivo SMS API into RedwoodJS apps. Send text messages with GraphQL mutations, secure credentials, error handling, and production-ready code examples.
Build a RedwoodJS application that sends SMS messages programmatically using the Plivo API. You'll create a backend service exposed via GraphQL and optionally trigger it from a basic frontend form.
By the end of this tutorial, you'll have a functional RedwoodJS application capable of sending SMS messages via Plivo, complete with secure credential management and basic error handling.
Project Overview and Goals
What You're Building:
Construct a RedwoodJS application featuring:
- An API service (
sms) responsible for interacting with the Plivo API. - A GraphQL mutation (
sendSms) to trigger the SMS sending functionality. - Secure handling of Plivo API credentials using environment variables.
- (Optional) A simple web interface (
SendSmsPage) to input a recipient number and message body, then trigger the GraphQL mutation.
Problem Solved:
Add programmatic SMS sending capabilities to your RedwoodJS applications for notifications, alerts, one-time passwords (OTPs), or other communication needs, leveraging Plivo's reliable infrastructure.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. It provides structure and tooling for building both frontend (React) and backend (Node.js, GraphQL, Prisma) components. Chosen for its integrated setup and developer experience.
- Node.js: The underlying runtime environment for the RedwoodJS API side.
- Plivo: A cloud communications platform providing APIs for SMS, voice, and more. Chosen for its robust SMS API and Node.js SDK.
- GraphQL: Used by RedwoodJS for API interactions between the web and api sides.
- TypeScript: For type safety and improved developer experience within RedwoodJS.
- Yarn: The package manager used by RedwoodJS.
System Architecture:
graph LR
A[User Browser (React Frontend)] -- GraphQL Mutation --> B(RedwoodJS API - GraphQL Server);
B -- Calls Service --> C(RedwoodJS Service - sms.ts);
C -- Uses Plivo SDK --> D(Plivo API);
D -- Sends SMS --> E(Mobile Network);
E -- Delivers --> F(Recipient Phone);
subgraph RedwoodJS Application
A
B
C
end
subgraph External Services
D
E
F
endPrerequisites:
- Node.js (v18 or later recommended)
- Yarn (v1 or Classic)
- A Plivo account (Sign up here)
- Your Plivo Auth ID and Auth Token (found on your Plivo Console dashboard: https://console.plivo.com/dashboard/)
- An SMS-enabled Plivo phone number (purchase one from the Plivo Console: Phone Numbers > Buy Numbers) or a registered Alphanumeric Sender ID (where applicable). Note that sending to US/Canada requires a Plivo phone number.
- Basic understanding of RedwoodJS concepts (web/api sides, services, GraphQL).
- A mobile phone number to receive test messages (If using a Plivo trial account, this number must be verified as a sandbox number in the Plivo console: Phone Numbers > Sandbox Numbers).
Final Outcome:
A RedwoodJS application where you can trigger an SMS message to be sent to a specified phone number via a GraphQL mutation, using Plivo credentials securely stored in environment variables.
1. Setting up the Project
Start by creating a new RedwoodJS project and installing the necessary Plivo dependency.
1.1 Create RedwoodJS Project
Open your terminal and run the RedwoodJS create command:
# Ensure you have the latest RedwoodJS CLI
# npm install -g @redwoodjs/cli
yarn create redwood-app ./redwood-plivo-sms --tsThis command scaffolds a new RedwoodJS project with TypeScript enabled in a directory named redwood-plivo-sms.
1.2 Navigate to Project Directory
cd redwood-plivo-sms1.3 Understand Project Structure
RedwoodJS separates frontend (web) and backend (api) concerns:
web/: Contains your React frontend code (Pages, Components, Cells, Layouts).api/: Contains your backend code (GraphQL schema, Services, Functions, Prisma schema).
Your Plivo integration logic will reside primarily in the api side.
1.4 Install Plivo Node.js SDK
The Plivo SDK simplifies interaction with their API. Install it specifically within the api workspace:
yarn workspace api add plivo1.5 Configure Environment Variables
Never hardcode sensitive credentials like API keys. RedwoodJS uses .env files for environment variables.
Create a .env file in the root of your project:
touch .envAdd your Plivo credentials and sender number to this file:
# .env
# Plivo Credentials – Get from https://console.plivo.com/dashboard/
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
# Plivo Sender Number – Get from https://console.plivo.com/numbers/
# Must be an SMS-enabled number purchased from Plivo, in E.164 format (e.g., +14155551234)
# Or a registered Alphanumeric Sender ID where applicable/allowed.
PLIVO_SENDER_NUMBER=YOUR_PLIVO_SENDER_NUMBER- How to get values:
PLIVO_AUTH_ID&PLIVO_AUTH_TOKEN: Log in to your Plivo Console. They're displayed prominently on the main dashboard.PLIVO_SENDER_NUMBER: Navigate to "Phone Numbers" in the Plivo Console. If you don't have one, click "Buy Numbers" and purchase an SMS-enabled number. Ensure it's in E.164 format (e.g.,+12025551234).
- Security: Ensure
.envis listed in your.gitignorefile (RedwoodJS includes this by default) to prevent accidentally committing secrets. - Purpose: Using environment variables keeps secrets out of the codebase and allows for different configurations per environment (development, staging, production).
2. Implementing Core Functionality (Plivo Service)
RedwoodJS services encapsulate business logic. Create an sms service to handle sending messages via Plivo.
2.1 Generate the Service
Use the RedwoodJS CLI to generate the service files:
yarn rw g service sms --tsThis creates:
api/src/services/sms/sms.ts: Where your Plivo logic will live.api/src/services/sms/sms.test.ts: For writing unit tests (covered later).
2.2 Implement the sendSms Function
Open api/src/services/sms/sms.ts and replace its contents with the following:
// api/src/services/sms/sms.ts
import plivo from 'plivo' // Standard import based on official plivo-node SDK
// Reference: https://github.com/plivo/plivo-node
import { logger } from 'src/lib/logger'
interface SendSmsInput {
to: string
body: string
}
interface SendSmsResponse {
success: boolean
messageId?: string // Plivo returns message_uuid array
error?: string
}
// Initialize Plivo client outside the function for reuse
// The Plivo SDK automatically reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN from environment variables
// when initialized with no arguments, as documented at https://github.com/plivo/plivo-node#authentication
let client: plivo.Client | null = null;
try {
client = new plivo.Client(
process.env.PLIVO_AUTH_ID,
process.env.PLIVO_AUTH_TOKEN
);
} catch (e) {
logger.error({ err: e }, "Failed to initialize Plivo Client. Check Auth ID/Token.");
}
/**
* Sends an SMS message using the Plivo API.
*
* @param to The destination phone number in E.164 format (e.g., +14155551234).
* @param body The text content of the SMS message.
* @returns Promise<SendSmsResponse> An object indicating success status, message ID, or error.
*/
export const sendSms = async ({
to,
body,
}: SendSmsInput): Promise<SendSmsResponse> => {
logger.info({ custom: { to, bodyLength: body.length } }, 'Attempting to send SMS')
if (!client) {
logger.error('Plivo client is not initialized. Cannot send SMS.');
return { success: false, error: 'SMS service configuration error.' };
}
// Basic validation (consider adding more robust validation, e.g., E.164 format check)
if (!to || !body) {
logger.error('Missing "to" or "body" for sendSms')
return { success: false, error: 'Recipient number and message body are required.' }
}
if (!process.env.PLIVO_SENDER_NUMBER) {
logger.error('PLIVO_SENDER_NUMBER environment variable is not set.')
return { success: false, error: 'Sender number is not configured.' }
}
try {
// Plivo Node.js SDK messages.create() method signature based on official documentation
// Reference: https://github.com/plivo/plivo-node
// Parameters: { src, dst, text, [optional params] }
const response = await client.messages.create({
src: process.env.PLIVO_SENDER_NUMBER,
dst: to,
text: body
// Optional parameters: { type: 'sms', url: 'callback_url', method: 'POST', log: true }
// See Plivo API documentation for complete parameter list
})
logger.info({ custom: { response } }, 'Plivo API response received')
// Plivo returns messageUuid array for successfully queued messages
// Response structure: { messageUuid: ['uuid-string'], message: 'message(s) queued' }
if (response && response.messageUuid && response.messageUuid.length > 0) {
logger.info(`SMS sent successfully to ${to}. Message UUID: ${response.messageUuid[0]}`)
return { success: true, messageId: response.messageUuid[0] }
} else {
logger.warn({ custom: { response } }, 'Plivo response indicates potential issue (e.g., success without message UUID)')
const responseDetails = response ? JSON.stringify(response) : 'No response details';
return { success: false, error: `SMS potentially not sent. Check Plivo logs. Response: ${responseDetails}` }
}
} catch (error) {
logger.error({ err: error }, 'Error sending SMS via Plivo')
let errorMessage = 'Failed to send SMS via Plivo.'
if (error instanceof Error) {
errorMessage = error.message
}
return { success: false, error: errorMessage }
}
}
// Note: If you define other functions related to SMS (e.g., getStatus), add them here.
// Ensure your GraphQL SDL reflects any functions you want to expose via the API.Explanation:
- Imports: Import the Plivo SDK using the standard
plivoimport as documented in the official plivo-node repository (https://github.com/plivo/plivo-node). - Interfaces: Define types for input (
SendSmsInput) and output (SendSmsResponse). - Plivo Client Initialization: Create the
plivo.Clientinstance using credentials fromprocess.env. The SDK automatically readsPLIVO_AUTH_IDandPLIVO_AUTH_TOKENenvironment variables when no arguments are provided to the constructor. sendSmsFunction:- Takes
toandbody. - Checks if the client was initialized.
- Performs basic validation.
- Uses
try...catchfor errors during the API call. - Calls
client.messages.create()with an object containingsrc,dst, andtextparameters as documented in the official SDK. - Logs information and errors.
- Parses the Plivo response, looking for
messageUuidarray in the response structure. - Returns a
SendSmsResponse.
- Takes
3. Building the API Layer (GraphQL Mutation)
Expose your sendSms service function by defining a GraphQL mutation.
3.1 Define the GraphQL Schema
Open api/src/graphql/sms.sdl.ts (create it if it doesn't exist, replacing any default redwood.sdl.ts). Add the following:
// api/src/graphql/sms.sdl.ts
export const schema = gql`
# Define the response structure for the mutation
type SmsResponse {
success: Boolean!
messageId: String # Can be null if sending failed before getting an ID
error: String # Can be null on success
}
# Define the input parameters for the mutation
# Note: Redwood maps service function arguments directly by default.
type Mutation {
"""
Sends an SMS message via Plivo.
Requires 'to' (E.164 format) and 'body' (message text).
"""
sendSms(to: String!, body: String!): SmsResponse!
@skipAuth # SECURITY CRITICAL: For testing ONLY. Replace with @requireAuth in production!
# To secure this endpoint, @skipAuth MUST be replaced with @requireAuth
# and authentication must be implemented in your RedwoodJS app.
# Exposing this without auth allows *anyone* to send SMS via your Plivo account.
# See RedwoodJS auth docs: https://redwoodjs.com/docs/auth
}
`Explanation:
SmsResponseType: Defines the mutation's return structure.MutationType: Defines thesendSmsmutation withtoandbodyarguments.@skipAuth: Security Warning! This directive bypasses authentication for easy testing. Replace it with@requireAuth(or a role-based directive) and set up RedwoodJS authentication before deploying to production. Failure to do so creates a major security vulnerability.
3.2 RedwoodJS Magic
Redwood automatically maps the sendSms mutation to the sendSms function in api/src/services/sms/sms.ts.
3.3 Testing with GraphQL Playground
Start the development server:
yarn rw devNavigate to http://localhost:8911/graphql. Test the mutation:
# GraphQL Mutation Query (for testing in Playground)
mutation SendTestSms {
sendSms(to: "+1XXXXXXXXXX", body: "Hello from RedwoodJS and Plivo!") {
success
messageId
error
}
}- Replace
+1XXXXXXXXXXwith your verified sandbox number (trial) or any destination (paid). - Execute the mutation, check the response, check your phone, and review terminal/Plivo logs.
4. Integrating with Plivo – Deeper Dive
- API Credentials: Find these on your Plivo Console Dashboard (https://console.plivo.com/dashboard/). Keep
AUTH_TOKENsecret. Store in.env/ hosting environment variables. - Plivo Phone Number / Sender ID: Manage in Plivo Console > Phone Numbers. Must be SMS-enabled. Use E.164 format (
+1...). For sending Application-to-Person (A2P) SMS to US numbers using local (10-digit long code) numbers, 10DLC registration is required. Consult Plivo's 10DLC documentation (https://www.plivo.com/docs/sms/guides/10dlc) for compliance details. - Trial Account Limitations: Can only send to verified Sandbox Numbers. Messages are prefixed with
[Sent from Plivo trial account]. Upgrade to remove limits. - E.164 Format: Essential (
+country code, number). International standard format for phone numbers.
5. Error Handling, Logging, and Retries
-
Error Handling Strategy: Use
try...catchin the service. Return structuredSmsResponsewithsuccess: falseanderrormessage. Log detailed errors internally. -
Logging: Use Redwood's
logger(import { logger } from 'src/lib/logger'). Log info (successes, IDs), warnings (unexpected but non-critical issues), and errors (exceptions, failures). Include error objects ({ err: error }) inlogger.error. Use log aggregation services in production. -
Retry Mechanisms: Consider implementing retries for transient network/Plivo errors. Use libraries like
async-retry. Implement exponential backoff. Only retry retryable errors (e.g., network, 5xx), not permanent ones (4xx like auth failure, invalid number).Note: The functions
isPermanentPlivoError,isRetryableNetworkError, andisRetryablePlivoErrorin the example below are placeholders. Implement the logic for these based on the specific error types thrown by the Plivo SDK (check its documentation for error codes and types) and potential network errors.typescript// Conceptual Retry Logic (requires installing 'async-retry': yarn workspace api add async-retry) import retry from 'async-retry'; import plivo from 'plivo'; // Standard import import { logger } from 'src/lib/logger' // Assume SendSmsResponse/Input interfaces are defined above // --- Placeholder functions - IMPLEMENT THESE --- function isPermanentPlivoError(error: any): boolean { // Example: Check for specific Plivo status codes like 400, 401, 404 // Plivo errors may include statusCode property if (error?.statusCode) { return error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429; } return false; } function isRetryableNetworkError(error: any): boolean { // Example: Check for generic network error types (e.g., ECONNRESET, ETIMEDOUT) if (error instanceof Error) { return ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'].includes((error as any).code); } return false; } function isRetryablePlivoError(error: any): boolean { // Example: Check for specific Plivo status codes like 429 (Rate Limit), 5xx (Server Error) if (error?.statusCode) { return error.statusCode === 429 || error.statusCode >= 500; } return false; } // --- End Placeholder functions --- // Example usage within a modified sendSms function: export const sendSmsWithRetry = async ({ to, body, }: SendSmsInput): Promise<SendSmsResponse> => { logger.info({ custom: { to, bodyLength: body.length } }, 'Attempting to send SMS with retry logic') if (!client) { logger.error('Plivo client is not initialized. Cannot send SMS.'); return { success: false, error: 'SMS service configuration error.' }; } if (!to || !body) { logger.error('Missing "to" or "body" for sendSms') return { success: false, error: 'Recipient number and message body are required.' } } if (!process.env.PLIVO_SENDER_NUMBER) { logger.error('PLIVO_SENDER_NUMBER environment variable is not set.') return { success: false, error: 'Sender number is not configured.' } } try { const response = await retry( async (bail, attempt) => { logger.info(`Attempt ${attempt} to send SMS via Plivo...`); try { if (!client) throw new Error("Plivo client not initialized"); const plivoResponse = await client.messages.create({ src: process.env.PLIVO_SENDER_NUMBER!, dst: to, text: body }); return plivoResponse; } catch (error) { logger.warn({ err: error }, `Attempt ${attempt} failed`); if (isPermanentPlivoError(error) || (!isRetryableNetworkError(error) && !isRetryablePlivoError(error))) { logger.error({ err: error }, 'Permanent or non-retryable error encountered, stopping retries.'); bail(error as Error); return; } throw error; } }, { retries: 3, factor: 2, minTimeout: 1000, onRetry: (error, attempt) => { logger.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`); } } ); logger.info({ custom: { response } }, 'Plivo API response received after retries (if any)'); if (response && response.messageUuid && response.messageUuid.length > 0) { logger.info(`SMS sent successfully to ${to}. Message UUID: ${response.messageUuid[0]}`); return { success: true, messageId: response.messageUuid[0] }; } else { logger.warn({ custom: { response } }, 'Plivo response indicates potential issue post-retry'); const responseDetails = response ? JSON.stringify(response) : 'No response details'; return { success: false, error: `SMS potentially not sent post-retry. Check Plivo logs. Response: ${responseDetails}` }; } } catch (error) { logger.error({ err: error }, 'Error sending SMS via Plivo after all retries'); let finalErrorMessage = 'Failed to send SMS after multiple attempts.'; if (error instanceof Error) { finalErrorMessage += ` Last error: ${error.message}`; } return { success: false, error: finalErrorMessage }; } } // --- End conceptual retry logic --- -
Testing Errors: Use invalid credentials, numbers, disconnect network, or mock the Plivo client in tests to throw errors.
6. Database Schema and Data Layer
This example doesn't require database interaction.
- When Needed: Track SMS history, status (via webhooks), or associate messages with users.
- Implementation:
- Define a model in
api/db/schema.prisma(e.g.,SmsMessagemodel withplivoMsgId,to,from,body,status, timestamps). - Run
yarn rw prisma migrate dev. - Use the
dbclient (import { db } from 'src/lib/db') in the service todb.smsMessage.create({...})after a successful Plivo call.
- Define a model in
- Data Access: Services handle database interactions.
- Migrations: Use
yarn rw prisma migrate dev(dev) andyarn rw prisma migrate deploy(production).
7. Adding Security Features
-
Secure Credential Storage: Use
.env(local),.gitignore, and hosting provider environment variables (production). -
Input Validation & Sanitization:
- GraphQL types provide basic checks.
- Add service-level validation: E.164 regex for
to, length checks forbody.
typescript// Example E.164 validation in service const e164Pattern = /^\+[1-9]\d{1,14}$/; if (!e164Pattern.test(to)) { logger.error(`Invalid E.164 format for 'to' number: ${to}`); return { success: false, error: 'Invalid recipient phone number format. Use E.164 (e.g., +12025551234).' }; } -
Authentication & Authorization:
- CRITICAL: Replace
@skipAuthwith@requireAuth. - Implement Redwood auth (dbAuth, etc.). See Redwood Auth Docs.
- Ensure only intended users can trigger
sendSms.
- CRITICAL: Replace
-
Rate Limiting: Prevent abuse. Implement at API Gateway, Load Balancer, or custom middleware (complex in default Redwood setup). Plivo has limits, but don't rely solely on them.
-
Common Vulnerabilities: Input validation prevents injection if data is misused elsewhere. Authentication and authorization are key.
8. Handling Special Cases
- E.164 Format: Essential –
+country code, number. - Character Encoding & Concatenation: GSM-7 (160 chars) vs. Unicode (UCS-2, 70 chars/SMS for emojis). Long messages are split (concatenated) by Plivo, using more segments (affects cost). See Plivo docs.
- International Sending: Check country regulations (Sender ID rules, opt-in, content). Alphanumeric Sender IDs may need registration and might not support replies. US/Canada usually require Plivo numbers.
- Delivery Reports (Advanced): Configure Plivo webhooks to send status updates (delivered, failed) to a Redwood Function endpoint. Store status in your database. Requires
urlparameter inmessages.createor Plivo app config.
9. Implementing Performance Optimizations
Performance is usually dominated by Plivo API response time for single sends. Relevant for bulk operations.
-
Sending Multiple Messages:
-
Parallel Requests (Different Messages): Use
Promise.allfor independent messages to different recipients. Be mindful of Plivo rate/concurrency limits.typescript// Example: Sending personalized messages in parallel // const sendPromises = recipients.map(r => sendSms({ to: r.phone, body: ... })); // const results = await Promise.all(sendPromises); -
Plivo Bulk SMS (Same Message): Send the same message to multiple numbers (up to Plivo's limit, often 10) by joining numbers with
<in thedstfield. More efficient than multiple API calls. Modify the service to handle array input.typescript// Conceptual modification for Plivo bulk (same message) // Input: { to: ['+1...', '+1...'], body: '...' } // if (Array.isArray(to) && to.length > 1 && to.length <= 10) { // Check Plivo limit // const destinationString = to.join('<'); // // Call client.messages.create with destinationString as dst // } ...
-
-
Resource Usage: Ensure the Plivo client isn't re-initialized needlessly (module scope initialization is generally fine if env vars are loaded at startup).
10. Monitoring, Observability, and Analytics
- Health Checks: Create a simple GraphQL query (
ping) or dedicated function (yarn rw g function health) for uptime monitoring. - Performance Metrics: Track
sendSmsduration (logger/APM). Monitor Plivo API latency via the Plivo dashboard/logs. - Error Tracking: Integrate Sentry, Bugsnag, etc. (
yarn rw setup deploy sentry). Provides better error analysis than logs alone. - Plivo Dashboard & Logs: Check Plivo Console > Messaging > Logs for message status (Sent, Delivered, Failed, etc.) and error codes. Use Plivo analytics.
- Custom Dashboards/Alerts: If storing status in your database, build internal dashboards (send volume, success/failure rates). Alert on high error rates via error tracking/logging tools.
11. Troubleshooting and Caveats
- Authentication Failure (Plivo 401): Check
.env/ hosting env vars forPLIVO_AUTH_ID/TOKEN. Ensure they're loaded correctly. - Invalid Destination Number: Verify E.164 format (
+...). Confirm number validity. - Sender Number Not Owned / Not SMS Enabled: Check
PLIVO_SENDER_NUMBERin.env/ env vars. Verify number ownership and SMS capability in Plivo Console. - Trial Account Restriction: Sending to a non-sandbox number? Add the number to Sandbox Numbers or upgrade your Plivo account.
- Insufficient Funds: Add Plivo credits.
- Rate Limits Exceeded (Plivo 429): Implement app-side rate limiting/throttling. Contact Plivo for limit increases if needed.
- Environment Variables Not Loaded: Ensure
.env(local) or hosting provider settings (deployed) are correct and the app is restarted/reloaded. - Plivo SDK Issues: Check Plivo Node.js SDK docs for your installed version. Verify method calls/parameters. Lock the SDK version in
package.jsonif needed.
12. Deployment and CI/CD
- Deployment Platforms: Vercel, Netlify, Render, AWS Serverless, etc. See Redwood Deployment Docs.
- Environment Variables: CRITICAL: Set
PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN,PLIVO_SENDER_NUMBERin your hosting provider's settings. - Build Command:
yarn rw deploy <provider>. - Database Migrations (If Used): Run
yarn rw prisma migrate deployin your deployment pipeline. - CI/CD: Automate tests, build, migrations, deploy (GitHub Actions, etc.).
- Rollback: Understand your provider's rollback mechanism. Use Git tags.
13. Verification and Testing
-
Unit Testing (Service): Use Jest and mock the Plivo client (
api/src/services/sms/sms.test.ts). Test success, error handling, input validation, and correct Plivo method calls.typescript// api/src/services/sms/sms.test.ts import { sendSms } from './sms' // Mock the Plivo client and its methods const mockCreate = jest.fn() jest.mock('plivo', () => { return { // Mock the default export containing the Client constructor default: { Client: jest.fn().mockImplementation(() => { return { messages: { create: mockCreate } } }) }, // Also mock named export if needed Client: jest.fn().mockImplementation(() => { return { messages: { create: mockCreate } } }) } }) // Mock logger to prevent actual logging during tests jest.mock('src/lib/logger', () => ({ logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() } })) describe('sms service', () => { const OLD_ENV = process.env; beforeEach(() => { jest.clearAllMocks(); process.env = { ...OLD_ENV, PLIVO_AUTH_ID: 'test-auth-id', PLIVO_AUTH_TOKEN: 'test-auth-token', PLIVO_SENDER_NUMBER: '+15551112222', }; }); afterAll(() => { process.env = OLD_ENV; }); it('should call Plivo create and return success on valid input', async () => { const mockResponse = { messageUuid: ['mock-uuid-123'], message: 'message(s) queued' } mockCreate.mockResolvedValue(mockResponse); const result = await sendSms({ to: '+15553334444', body: 'Test message' }); expect(mockCreate).toHaveBeenCalledWith({ src: '+15551112222', dst: '+15553334444', text: 'Test message' }); expect(result).toEqual({ success: true, messageId: 'mock-uuid-123' }); }); it('should return error if Plivo client throws an error', async () => { const mockError = new Error('Plivo API Error'); mockCreate.mockRejectedValue(mockError); const result = await sendSms({ to: '+15553334444', body: 'Test message' }); expect(mockCreate).toHaveBeenCalledTimes(1); expect(result).toEqual({ success: false, error: 'Plivo API Error' }); }); it('should return error if "to" number is missing', async () => { const result = await sendSms({ to: '', body: 'Test message' }); expect(mockCreate).not.toHaveBeenCalled(); expect(result).toEqual({ success: false, error: 'Recipient number and message body are required.' }); }); it('should return error if sender number env var is missing', async () => { delete process.env.PLIVO_SENDER_NUMBER; const result = await sendSms({ to: '+15553334444', body: 'Test message' }); expect(mockCreate).not.toHaveBeenCalled(); expect(result).toEqual({ success: false, error: 'Sender number is not configured.' }); }); });
Frequently Asked Questions
How do I initialize the Plivo client in a RedwoodJS service?
Import plivo and create a new plivo.Client() instance. The SDK automatically reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN from environment variables when you initialize the client without arguments. Store credentials in your .env file and initialize the client at module scope for reuse across function calls. See the official documentation at https://github.com/plivo/plivo-node#authentication for complete details.
What phone number format does Plivo require for sending SMS?
Plivo requires E.164 format for all phone numbers: + followed by the country code and number (e.g., +14155551234). This international standard ensures proper routing across global networks. Validate recipient numbers using the regex pattern /^\+[1-9]\d{1,14}$/ before sending to catch formatting errors early.
How do I expose the SMS service through GraphQL in RedwoodJS?
Create a GraphQL SDL file at api/src/graphql/sms.sdl.ts defining a sendSms mutation. RedwoodJS automatically maps the mutation to your service function by name. Define input parameters (to: String!, body: String!) and return type (SmsResponse). For testing, use @skipAuth directive, but replace with @requireAuth before production deployment.
How do I handle Plivo API errors in RedwoodJS services?
Wrap client.messages.create() calls in try...catch blocks. Return a structured response object with success: false and descriptive error messages. Use RedwoodJS logger (import { logger } from 'src/lib/logger') to log detailed error objects with logger.error({ err: error }, 'Error sending SMS'). For transient errors like network failures or 5xx responses, implement retry logic using libraries like async-retry with exponential backoff.
Can I test Plivo SMS integration locally without sending real messages?
Yes. Mock the Plivo client in your Jest tests using jest.mock('plivo') to return controlled responses. For integration testing, use a Plivo trial account which allows sending to verified sandbox numbers (add them in Plivo Console > Phone Numbers > Sandbox Numbers). Trial messages are prefixed with [Sent from Plivo trial account] and don't consume credits.
What environment variables do I need for Plivo in production?
Set three required environment variables in your hosting provider's settings: PLIVO_AUTH_ID (your Auth ID from the Plivo dashboard), PLIVO_AUTH_TOKEN (your Auth Token – keep this secret), and PLIVO_SENDER_NUMBER (your SMS-enabled Plivo number in E.164 format). Never commit these to version control. Verify they're loaded correctly by checking process.env values in your deployed environment.
Do I need 10DLC registration to send SMS to US numbers?
Yes. For Application-to-Person (A2P) SMS to US numbers using 10-digit long code numbers, 10DLC registration is required by US carriers to comply with regulations. Register your brand and use case through the Plivo Console. See Plivo's 10DLC documentation (https://www.plivo.com/docs/sms/guides/10dlc) for complete registration requirements and timelines. Alphanumeric sender IDs or toll-free numbers may have different requirements.
How do I secure the SMS endpoint before deploying to production?
Replace @skipAuth with @requireAuth directive in your GraphQL SDL. Implement RedwoodJS authentication using dbAuth, Auth0, or another provider (see https://redwoodjs.com/docs/auth). Add role-based access control if needed using custom directives like @requireAuth(roles: ["admin"]). Implement rate limiting at the API gateway or hosting provider level to prevent abuse. Always validate and sanitize user inputs in your service layer.