Send SMS with MessageBird in RedwoodJS: Complete Integration Guide
Build SMS functionality in RedwoodJS using MessageBird API. Complete guide covering GraphQL mutations, service layer implementation, E.164 validation, error handling, and production deployment.
Learn how to integrate MessageBird's SMS API into your RedwoodJS application using GraphQL and the service layer. This comprehensive guide walks you through building SMS functionality with proper E.164 phone validation, error handling, and production-ready architecture.
You'll implement a complete SMS service using RedwoodJS's backend architecture—perfect for sending OTP codes, transactional alerts, appointment reminders, and user notifications through MessageBird's reliable delivery platform.
Why RedwoodJS for SMS Integration?
RedwoodJS provides an opinionated full-stack architecture that simplifies SMS integration:
Service Layer Architecture: RedwoodJS services encapsulate business logic and third-party integrations cleanly, keeping your GraphQL resolvers thin and testable
Integrated GraphQL API: First-class GraphQL support through SDL (Schema Definition Language) with auto-generated resolvers, making SMS operations accessible from your React frontend
Environment Variable Management: Built-in .env support for secure API key storage and configuration across development, staging, and production environments
Developer Experience: Hot reloading, TypeScript support, and built-in testing tools streamline development and reduce integration time
TypeScript Support: Full TypeScript integration ensures type safety across your SMS service layer, GraphQL schema, and frontend components
Deployment Flexibility: Deploy to Vercel, Netlify, AWS, or traditional servers with RedwoodJS's adaptable build system
Prerequisites
Before you begin this integration, ensure you have the following:
RedwoodJS Application: A working RedwoodJS project (version 8.0+, latest stable is 8.8.1). If you're starting fresh, create a new project with yarn create redwoodjs-app my-app.
Node.js: Version 20 or higher installed on your development machine. RedwoodJS requires Node.js 20+ for optimal performance and compatibility.
Yarn: Version 1.22.21 or higher as your package manager. RedwoodJS uses Yarn for dependency management.
MessageBird Account: Sign up at MessageBird to get your API access key. You'll need this key for authentication.
MessageBird Access Key: Located in your MessageBird Dashboard under Developers → API Access. Keep this key secure and never commit it to version control.
MessageBird SDK Version Note: This guide uses messagebird SDK v4.0.1 (the official Node.js SDK last updated in 2022). While the package is 3+ years old, it remains stable and functional for SMS operations. MessageBird has not deprecated this SDK, and it's still actively used in production environments. However, be aware that maintenance updates are infrequent.
1. Project Setup
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
Create RedwoodJS Project:
Open your terminal and run:
Bash
yarn create redwood-app ./redwood-messagebird-sms
Navigate to Project Directory:
Bash
cd redwood-messagebird-sms
Install MessageBird SDK:
Add the official MessageBird Node.js SDK to the API side of your project:
Bash
yarn workspace api add messagebird
This command specifically installs the package within the api workspace, where our backend logic resides.
Configure Environment Variables:
Sensitive information like API keys should never be hardcoded. We'll use environment variables.
Create a .env file in the root of your project:
Bash
touch .env
Add your MessageBird Live API Access Key to the .env file. You can find this key in your MessageBird Dashboard under Developers -> API access.
Replace YOUR_LIVE_ACCESS_KEY with your actual key.
Important: Ensure .env is listed in your project's root .gitignore file (RedwoodJS includes this by default) to prevent accidentally committing your secret key.
Test vs. Live Keys: MessageBird provides both Live and Test API keys.
Live Key: Used for sending actual SMS messages. Incurs costs. Use this for production and real testing.
Test Key: Used for testing API integration syntax without sending real messages or incurring costs. API calls will appear successful but won't deliver SMS. Start with the Test key during initial development if preferred.
Code
* **Deployment:** When deploying (Section 12), you **must** configure this same environment variable (`MESSAGEBIRD_ACCESS_KEY`) in your hosting provider's settings.
This completes the basic project setup and configuration.
2. Implementing Core SMS Sending Functionality
RedwoodJS uses a service layer (api/src/services) for business logic. We'll create an sms service to handle interactions with the MessageBird API.
Generate the SMS Service:
Use the RedwoodJS CLI generator:
Bash
yarn rw g service sms
This creates:
api/src/services/sms/sms.ts: The service file where our logic will live.
api/src/services/sms/sms.scenarios.ts: For defining seed data for tests.
api/src/services/sms/sms.test.ts: The unit test file.
Implement the Sending Logic:
Open api/src/services/sms/sms.ts and replace its contents with the following:
TypeScript
// api/src/services/sms/sms.tsimport { initClient } from'messagebird'importtype { Messages } from'messagebird/types'// Import specific typesimport { logger } from'src/lib/logger'// Redwood's built-in loggerimport { UserInputError } from'@redwoodjs/graphql-server'// Redwood's standard error class// Define the expected input structure for clarity and type safetyinterfaceSendSmsInput {
originator: stringrecipient: stringbody: string
}
// Define the expected success response structureinterfaceSmsSendSuccessResponse {
success: booleanmessageId: string | null// MessageBird returns an ID on successstatus: string | null// e.g., 'sent', 'buffered'
}
// Initialize the MessageBird client using the API key from environment variables// Ensure MESSAGEBIRD_ACCESS_KEY is set in your .env fileconst accessKey = process.env.MESSAGEBIRD_ACCESS_KEYif (!accessKey) {
// Log error instead of throwing immediately during initialization phase// The error will be caught during the actual sendSms call if key is missing
logger.error('MESSAGEBIRD_ACCESS_KEY environment variable not set.')
// Or, depending on desired behavior:// throw new Error('MESSAGEBIRD_ACCESS_KEY environment variable not set.')
}
// Initialize client - it might still work even if key is missing temporarily// but API calls will fail later. Handle missing key in the function.const messagebird = initClient(accessKey || 'dummy-key-if-needed')
/**
* Sends an SMS message using the MessageBird API.
*
* @paramoriginator - The sender ID (phone number or alphanumeric string).
* @paramrecipient - The recipient's phone number in E.164 format (e.g., +14155552671).
* @parambody - The text content of the SMS message.
* @returns Promise resolving to SmsSendSuccessResponse on success.
* @throws UserInputError for validation errors or MessageBird API errors.
*/exportconst sendSms = async ({
originator,
recipient,
body,
}: SendSmsInput): Promise<SmsSendSuccessResponse> => {
// Check for Access Key availability here, inside the function callif (!accessKey) {
logger.error('MessageBird API Key (MESSAGEBIRD_ACCESS_KEY) is missing.');
thrownewError('SMS Service is not configured. Missing API Key.'); // Use generic Error for config issues
}
logger.info(
{ recipient: recipient, originator: originator }, // Avoid logging full body potentially'Attempting to send SMS via MessageBird'
)
// Basic Input Validation (Add more robust validation as needed)if (!originator || !recipient || !body) {
thrownewUserInputError('Originator, recipient, and body are required.')
}
// Very basic E.164 format check (improves robustness)if (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
thrownewUserInputError('Recipient phone number must be in E.164 format (e.g., +14155552671).')
}
if (body.length > 1600) { // Generous limit, but prevents huge payloads
logger.warn('SMS body exceeds typical limits, may be truncated or split.')
}
constparams: Messages.Params = {
originator: originator,
recipients: [recipient], // API expects an arraybody: body,
}
try {
const response = awaitnewPromise<Messages.Message>((resolve, reject) => {
messagebird.messages.create(params, (err, response) => {
if (err) {
logger.error({ err }, 'MessageBird API error')
// Map MessageBird errors to user-friendly errorslet errorMessage = 'Failed to send SMS due to an external service error.'if (err.errors && err.errors.length > 0) {
// Example: Extracting specific MessageBird error descriptions
errorMessage = `MessageBird Error: ${err.errors[0].description}`if (err.errors[0].code === 2) { // Example: Authentication error
errorMessage = 'MessageBird authentication failed. Check API Key.'
} elseif (err.errors[0].code === 21) { // Example: Bad request (often invalid number)
errorMessage = 'Invalid recipient number or originator format.'
}
}
returnreject(newUserInputError(errorMessage))
}
// Ensure response and recipients info exist before accessingif (response && response.recipients && response.recipients.items && response.recipients.items.length > 0) {
resolve(response)
} else {
logger.error({ response }, 'Unexpected response structure from MessageBird')
// Use generic Error for unexpected API responsesreject(newError('Invalid response received from MessageBird API.'))
}
})
})
// Assuming the first recipient item contains the relevant status/id for single sends.// This is generally safe for single-recipient messages.create calls.const firstRecipientInfo = response.recipients.items[0];
logger.info(
{ messageId: response.id, status: firstRecipientInfo.status },
'SMS sent successfully via MessageBird'
)
return {
success: true,
messageId: response.id || null, // Main message IDstatus: firstRecipientInfo.status || null// Status for the specific recipient
}
} catch (error) {
logger.error({ error }, 'Failed to send SMS')
// Re-throw UserInputErrors directly, wrap others if necessaryif (error instanceofUserInputError) {
throw error;
}
// Wrap unexpected errorsthrownewError(`An unexpected error occurred while sending SMS: ${error.message}`)
}
}
Explanation:
We import initClient from messagebird and necessary types.
We import Redwood's logger for structured logging and UserInputError for standard GraphQL errors.
We define TypeScript interfaces (SendSmsInput, SmsSendSuccessResponse) for better type safety and code clarity.
We retrieve the API key from process.env.MESSAGEBIRD_ACCESS_KEY and initialize the client. We now check for the key inside the sendSms function to provide a better error if it's missing during an actual attempt.
The sendSms function accepts an object with originator, recipient, and body.
Basic input validation is performed. Note: Robust phone number validation (E.164 format) using a library like libphonenumber-js is recommended for production.
We construct the params object required by messagebird.messages.create. Note that recipients must be an array.
The messagebird.messages.create method is asynchronous and uses a callback pattern. We wrap it in a Promise for cleaner async/await syntax.
Error Handling: Inside the callback, if err exists, we log the detailed error and reject the promise with a UserInputError, providing a more user-friendly message where possible by inspecting err.errors. MessageBird error codes (like 2 for auth or 21 for bad request) can be used for more specific feedback. Unexpected API response structures are handled with a generic Error.
Success Handling: If successful, we log the success and resolve the promise with the response. We extract the message ID and status for the first recipient, assuming a single recipient send.
The outer try...catch handles any unexpected errors during the promise execution or validation.
3. Building the API Layer (GraphQL Mutation)
RedwoodJS automatically maps service functions to GraphQL resolvers. We need to define the GraphQL schema (SDL) for our sendSms mutation.
Generate the SDL:
Use the RedwoodJS CLI generator:
Bash
yarn rw g sdl sms
This creates api/src/graphql/sms.sdl.ts.
Define the Mutation Schema:
Open api/src/graphql/sms.sdl.ts and replace its contents with the following:
GraphQL
# api/src/graphql/sms.sdl.ts
export const schema= gql`
""""""Input parameters for sending an SMS message.
""""""input SendSmsInput {""The sender ID (phone number or alphanumeric string).""originator: String!""The recipient's phone number in E.164 format (e.g., +14155552671).""recipient: String!""The text content of the SMS message (max ~1600 chars recommended).""body: String!}""""""
Response object after attempting to send an SMS.
Note: Errors are typically handled via the top-level 'errors' field in the GraphQL response.
""""""type SmsSendResponse {""Indicates whether the API call to MessageBird was initiated successfully by the service.""success: Boolean!""The unique ID assigned to the message by MessageBird (if successful).""messageId: String
""The status reported by MessageBird for the recipient (e.g., 'sent', 'buffered').""status: String
}""""""
Mutations related to sending SMS messages.
""""""typeMutation{""""""
Sends a single SMS message via MessageBird.
Requires originator, recipient (E.164 format), and body.
Throws errors for invalid input or API failures.
""""""
sendSms(input: SendSmsInput!): SmsSendResponse!@skipAuth# Or use @requireAuth}
`
Explanation:
We define an input SendSmsInput type mirroring the structure expected by our service function.
We define a type SmsSendResponse for the data returned by the mutation upon successful initiation, including success status, the MessageBird message ID, and the initial status.
Error Handling: Note that the error: String field has been removed. GraphQL standard practice is to handle errors through the top-level errors array in the response. RedwoodJS automatically populates this when a service function throws an Error (especially UserInputError). The client should check for the presence of this errors array.
We define the sendSms mutation within the Mutation type. It accepts the SendSmsInput and returns SmsSendResponse on success.
@skipAuth: For simplicity in this guide, we're skipping authentication. In a real application, you MUST protect this mutation using @requireAuth or similar directives to ensure only authorized users/systems can send SMS.
RedwoodJS automatically connects this SDL definition to the sendSms function in api/src/services/sms/sms.ts based on naming conventions.
4. Integrating with MessageBird (API Keys Recap)
While covered in setup, this section recaps the key aspects of MessageBird API key usage:
Obtain API Key:
Log in to your MessageBird Dashboard -> Developers -> API access.
Distinguish between Live API Key (sends real, paid SMS) and Test API Key (for syntax checks, no delivery).
Secure Storage & Purpose:
The key stored in the MESSAGEBIRD_ACCESS_KEY environment variable (in .env locally) is used by the sms service (process.env.MESSAGEBIRD_ACCESS_KEY) to authenticate requests.
The key is a long alphanumeric string provided by MessageBird.
Your local .env file is used during development (yarn rw dev).
When deploying (Section 12), you must configure this same environment variable (MESSAGEBIRD_ACCESS_KEY) in your hosting provider's settings (e.g., Vercel, Netlify Environment Variables). Do not commit your .env file.
5. Error Handling, Logging, and Retries
Our service implementation includes basic error handling and logging.
Error Handling Strategy:
Catch specific MessageBird API errors within the SDK callback.
Map known MessageBird error codes/descriptions to Redwood UserInputError for clearer feedback via GraphQL (returned in the errors array).
Catch unexpected errors (e.g., network issues, invalid SDK response) in the outer try...catch and typically throw a generic Error or potentially Redwood's FatalServerError.
Use UserInputError for invalid input or predictable API issues (bad number, auth fail).
Logging:
Use Redwood's built-in logger (import { logger } from 'src/lib/logger').
logger.info: Log successful attempts and outcomes, including the MessageBird message ID and status. Include key parameters like recipient/originator for context (avoid logging sensitive body content unless necessary and compliant).
logger.error: Log detailed error objects (err from MessageBird, error from catch blocks) to aid debugging. Include context like input parameters where safe.
Redwood automatically configures logging levels and outputs (console during development, potentially configurable for production).
Retry Mechanisms:
For a simple ""send SMS"" operation, automatic retries are often not recommended directly within this function, as it could lead to duplicate messages if the first request succeeded but the response failed.
If reliable delivery is critical and transient network errors are a concern, consider:
Idempotency: Check MessageBird's API documentation for idempotency key support to allow safe retries.
Background Jobs: Implement the SMS sending within a background job queue (e.g., using Redis and BullMQ integrated with Redwood). The job runner can handle retries with exponential backoff based on specific error types (e.g., network errors, rate limits) but not on errors indicating invalid input or successful sends where the response was lost. This is a more advanced setup beyond this basic guide.
6. Database Schema and Data Layer (Optional)
For this basic guide focused purely on sending an SMS, a database schema isn't strictly required. However, in a production application, you would likely want to log SMS sending attempts and their outcomes.
Potential Schema (Example using Prisma):
Prisma
// api/db/schema.prisma
model SmsLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
originator String
recipient String
body String? // Store potentially truncated body or omit for privacy
status String // e.g., 'attempted', 'sent', 'failed', 'delivered' (from webhook)
messageBirdId String? @unique // The ID returned by MessageBird
errorMessage String? // Store error message on failure
userId String? // Link to the user who triggered the SMS (if applicable)
// user User? @relation(fields: [userId], references: [id])
}
Implementation:
Add the model to schema.prisma.
Run yarn rw prisma migrate dev to apply changes.
In the sendSms service function:
Before calling MessageBird, create an SmsLog entry with status 'attempted'.
On success, update the entry with status 'sent' and the messageBirdId.
On failure, update the entry with status 'failed' and the errorMessage.
(Advanced) Set up MessageBird webhooks to receive Delivery Reports (DLRs) and update the status to 'delivered' or 'failed' based on the final outcome. This requires another API endpoint in Redwood to receive webhook events.
This database logging is omitted from the main code for simplicity but is a recommended practice.
7. Security Features
Protecting your SMS sending functionality is crucial.
Authentication/Authorization:
Protect the Mutation: As mentioned in Section 3, replace @skipAuth with @requireAuth (or role-based directives) on the sendSms mutation in api/src/graphql/sms.sdl.ts. This ensures only logged-in/authorized users or systems can trigger the SMS send. Refer to RedwoodJS Authentication documentation.
Input Validation and Sanitization:
Service Layer: Our service performs basic checks (required fields, E.164 format hint). Enhance this:
Use a library like libphonenumber-js for robust phone number validation and formatting.
Enforce stricter length limits on the body.
Validate the originator against allowed values (your purchased numbers or registered alphanumeric IDs).
Sanitization: Standard string handling is usually sufficient for SMS bodies. Ensure no unexpected characters could break the MessageBird API call.
API Key Security:
Handled via .env and never committing the file or exposing the key in frontend code.
Use distinct, restricted API keys in MessageBird if possible, limiting permissions if the key were ever compromised.
Rate Limiting:
Prevent abuse (accidental or malicious) that could rapidly deplete your MessageBird balance or get your numbers blocked.
Implement rate limiting on the sendSms mutation. Options:
RedwoodJS Directives: Create a custom directive using packages like rate-limiter-flexible with Redis or an in-memory store.
API Gateway: Configure rate limiting if using an external gateway.
Service Logic: Add checks within the sendSms service (e.g., query SmsLog timestamps per user/recipient).
Originator Restrictions:
Be aware of MessageBird and country-specific restrictions on using alphanumeric sender IDs vs. virtual mobile numbers (VMNs). Using an invalid originator format will cause API errors.
8. Handling Special Cases
Character Encoding & Limits:
Standard SMS (GSM 03.38): 160 characters/part.
Unicode (UCS-2) for non-GSM characters (e.g., emojis): 70 characters/part.
Long messages are automatically split by carriers (concatenated SMS), each part billed separately. MessageBird handles splitting. Our service includes a basic length warning.
Virtual Mobile Numbers (VMNs): Required for two-way communication and necessary in many countries. Use the full number in E.164 format (e.g., +14155552671) as the originator.
International Formatting:
Always use the E.164 format for recipient numbers (+ followed by country code and number, no spaces/dashes). Our basic validation hints at this. Use robust validation (e.g., libphonenumber-js).
Opt-Out Handling:
Regulations (like TCPA in the US) require handling STOP/OPT-OUT requests. MessageBird often manages this automatically for replies to VMNs. Ensure your application logic respects opt-out lists if managing them separately.
9. Performance Optimizations
For single transactional SMS, application performance is usually not the bottleneck.
API Call Latency: The primary latency is the network roundtrip to MessageBird. Ensure good server connectivity.
Asynchronous Processing: For non-time-critical SMS (e.g., bulk notifications), use a background job queue (see Section 5) to avoid blocking web requests.
Bulk Sending: MessageBird's API supports multiple recipients (up to 50 per call) in the recipients array. Modify the service if batch sending is needed. Avoid many sequential individual API calls.
10. Monitoring, Observability, and Analytics
Logging: Use structured logs (Section 5) and aggregate them in production (Datadog, Logtail, Papertrail).
Error Tracking: Integrate services like Sentry or Bugsnag with your Redwood API.
MessageBird Dashboard: Monitor SMS logs (status, cost, errors) and analytics (delivery rates) provided by MessageBird.
Health Checks: Use Redwood's default /graphql/health endpoint or create custom checks.
Custom Metrics (Advanced): Push metrics (sends, failures, latency) to monitoring systems (Prometheus, Datadog) from the service.
recipient is invalid / originator is invalid (Code 21): Check E.164 format for recipient; check originator validity (VMN format or registered Alphanumeric ID per country rules).
No balance (Code 9): Add MessageBird credits.
Message body is empty: Ensure body is provided.
HTTP 401 Unauthorized: API key issue.
HTTP 400 Bad Request: Invalid parameters; check MessageBird error details in logs.
Caveats:
Test vs. Live Keys: Remember Test keys don't deliver SMS. Use Live key in production.
Originator Restrictions: Alphanumeric IDs are not universally supported. VMNs are often required.
Delivery Reports (DLRs): Initial API success (sent) doesn't guarantee final delivery. Use MessageBird webhooks for DLRs (delivered, failed).
Rate Limits: Be aware of MessageBird API rate limits.
Compliance: Adhere to local SMS regulations (TCPA, GDPR, etc.).
12. Deployment and CI/CD
Choose Hosting Provider: Vercel, Netlify, Render, etc.
Configure Environment Variable: Set MESSAGEBIRD_ACCESS_KEY with your Live key in your hosting provider's environment variable settings. Ensure it's available to the API service at runtime (and potentially build time).
Deployment Commands: Use standard Redwood deploy commands (e.g., yarn rw deploy vercel).
CI/CD: Integrate deployment into your pipeline (GitHub Actions, etc.), securely injecting the MESSAGEBIRD_ACCESS_KEY secret. Include tests (yarn rw test api).
Rollback: Use your hosting provider's rollback features if needed.
13. Verification and Testing
Local Development Testing:
Run yarn rw dev.
Use the GraphQL playground (http://localhost:8911/graphql) to execute the sendSms mutation:
GraphQL
mutation SendTestSms {
sendSms(input:{originator:""+1YOUR_MESSAGEBIRD_NUMBER""# Or valid Alphanumeric IDrecipient:""+1RECIPIENT_PHONE_NUMBER""# Your test phone numberbody:""Hello from RedwoodJS and MessageBird!""}){
success
messageId
status
}}
Replace placeholders. Check the GraphQL response (expect data, not errors). Check API logs. Check the phone (if using Live key). Check MessageBird Dashboard Log.
Automated Testing (Unit/Integration):
Use Jest mocking for the MessageBird SDK in api/src/services/sms/sms.test.ts.
TypeScript
// api/src/services/sms/sms.test.tsimport { sendSms } from'./sms'import { initClient } from'messagebird'// Import to mockimport { UserInputError } from'@redwoodjs/graphql-server'// Mock the MessageBird SDKtypeMockMessageBirdClient = {
messages: {
create: jest.Mock
}
}
// Hold the mock implementation detailsletmockMessagesCreate: jest.Mock;
jest.mock('messagebird', () => {
mockMessagesCreate = jest.fn((params, callback) => {
// Default success mock implementationcallback(null, {
id: 'mock-message-id-123',
recipients: {
totalCount: 1,
totalSentCount: 1,
totalDeliveredCount: 0,
totalDeliveryFailedCount: 0,
items: [{
recipient: params.recipients[0],
status: 'sent',
statusDatetime: newDate().toISOString(),
messagePartCount: 1,
}]
}
})
});
return {
initClient: jest.fn().mockImplementation((accessKey) => {
// Basic check if access key is provided during init simulationif (!accessKey || accessKey === 'dummy-key-if-needed') {
// Simulate client init even without key, but create will fail if key needed
}
return { // Return the mocked client structuremessages: {
create: mockMessagesCreate,
},
}
}),
}
})
describe('sms service', () => {
constOLD_ENV = process.env;
beforeEach(() => {
// Reset mocks and environment variables before each test
jest.clearAllMocks();
// Ensure the env var is set for most tests
process.env = { ...OLD_ENV, MESSAGEBIRD_ACCESS_KEY: 'test-key-is-set' };
// Reset mock implementation to default success
mockMessagesCreate.mockImplementation((params, callback) => {
callback(null, { id: 'mock-message-id-123', recipients: { items: [{ status: 'sent' }] } });
});
});
afterAll(() => {
process.env = OLD_ENV; // Restore old environment
});
it('sends an SMS successfully', async () => {
const input = {
originator: '+15550001111',
recipient: '+15552223333',
body: 'Test message',
}
const result = awaitsendSms(input)
expect(mockMessagesCreate).toHaveBeenCalledTimes(1)
expect(mockMessagesCreate).toHaveBeenCalledWith(
{
originator: input.originator,
recipients: [input.recipient],
body: input.body,
},
expect.any(Function) // Callback function
)
expect(result).toEqual({
success: true,
messageId: 'mock-message-id-123',
status: 'sent', // From the mocked response
})
})
it('throws UserInputError for missing parameters', async () => {
const input = { originator: '+15550001111', recipient: '', body: 'Test' };
awaitexpect(sendSms(input)).rejects.toThrow(UserInputError);
awaitexpect(sendSms(input)).rejects.toThrow('Originator, recipient, and body are required.');
})
it('throws UserInputError for invalid recipient format', async () => {
const input = { originator: '+15550001111', recipient: 'invalid-number', body: 'Test' };
awaitexpect(sendSms(input)).rejects.toThrow(UserInputError);
awaitexpect(sendSms(input)).rejects.toThrow('Recipient phone number must be in E.164 format');
})
})
How the RedwoodJS SMS Integration Works
Let's break down the key components:
GraphQL SDL (Schema Definition Language)
The GraphQL SDL (Schema Definition Language) defines the structure of our GraphQL API. In this case, we have a sendSms mutation that accepts a SendSmsInput and returns a SmsSendResponse.
Service Implementation
The service implementation in api/src/services/sms/sms.ts is where the actual MessageBird API calls are made. It validates input, constructs the request, and handles the response from the MessageBird API.
E.164 Phone Number Format
MessageBird requires international phone numbers in E.164 format: +[country code][subscriber number] without spaces or special characters.
E.164 Format Rules:
Starts with + followed by 1-3 digit country code (e.g., +1 for USA/Canada, +44 for UK)
Maximum 15 digits total (including country code)
No spaces, parentheses, or hyphens—only digits after the + sign
Our service validates E.164 format using regex: /^\+[1-9]\d{1,14}$/ to catch malformed numbers before making expensive API calls.
SMS Character Limits and Encoding
Understanding SMS character limits prevents message truncation and unexpected costs:
GSM-7 Encoding (standard): 160 characters for a single message. Multi-part messages use 153 characters per segment (7 characters reserved for concatenation headers).
UCS-2/Unicode Encoding (emojis, non-Latin scripts): 70 characters for a single message. Multi-part messages use 67 characters per segment.
MessageBird automatically handles message segmentation. If your message exceeds these limits, it will be split into multiple segments, each billed separately. Plan your message content accordingly.
Common Character Limit Scenarios:
Standard English text (no special chars): 160 characters max for single SMS
Text with emoji or Arabic/Chinese/Cyrillic: 70 characters max for single SMS
Messages exceeding limits: Automatically split into segments with slight overhead
Deploying Your RedwoodJS SMS Integration to Production
When deploying your SMS integration to production:
Environment Variables
Ensure your MessageBird API key is securely stored in your production environment. Use RedwoodJS's .env file for development and configure your hosting provider's environment variables for production.
Error Handling and Logging
Implement robust error handling and logging to monitor SMS delivery and troubleshoot issues. Use RedwoodJS's logger for structured logging and integrate with monitoring tools like Sentry for error tracking.
Rate Limiting and Cost Management
SMS providers charge per message sent. Protect your application from abuse and unexpected costs:
Rate Limiting: Use middleware like express-rate-limit or implement custom rate limiting in your GraphQL context to restrict SMS sends per user/IP
User Authentication: Require authentication for the sendSms mutation using RedwoodJS's @requireAuth directive to prevent anonymous abuse
Daily/Monthly Caps: Track SMS usage per user in your database (via Prisma) and enforce daily or monthly sending limits
Cost Monitoring: Set up MessageBird billing alerts and monitor your SMS dashboard for unexpected usage spikes
Version Compatibility
RedwoodJS: This guide is compatible with RedwoodJS v8.0+. The latest stable version is 8.8.1 as of 2025.
Node.js: Requires Node.js 20+ for RedwoodJS compatibility and optimal performance.
MessageBird SDK: Uses v4.0.1, which is stable but infrequently updated. Monitor MessageBird's official repositories for any future SDK releases or deprecation notices.
How do I send SMS messages from a RedwoodJS application?
Install the MessageBird SDK (yarn workspace api add messagebird), create a GraphQL SDL schema defining a sendSms mutation, and implement a service in api/src/services/sms/sms.ts that calls MessageBird's API. Use RedwoodJS environment variables to store your MessageBird API key securely.
What phone number format does MessageBird require?
MessageBird requires E.164 international format: +[country code][subscriber number] with no spaces or special characters. Examples: +14155552671 (US), +442071234567 (UK). Validate using the regex pattern /^\+[1-9]\d{1,14}$/.
How do I handle MessageBird API errors in RedwoodJS?
Wrap MessageBird API calls in try-catch blocks or Promise rejection handlers. Map MessageBird error codes (like code 2 for authentication failures, code 21 for invalid numbers) to user-friendly error messages using RedwoodJS's UserInputError or ForbiddenError classes.
Can I use MessageBird test keys in RedwoodJS development?
Yes. MessageBird provides test API keys (prefix test_) that simulate SMS sending without actually delivering messages or charging your account. Set MESSAGEBIRD_ACCESS_KEY=test_... in your .env file for development and testing.
How do I prevent SMS abuse in my RedwoodJS application?
Implement rate limiting using middleware, require user authentication with RedwoodJS's @requireAuth directive, enforce per-user daily SMS caps by tracking sends in your Prisma database, and monitor MessageBird's dashboard for unusual activity.
What's the difference between RedwoodJS services and GraphQL resolvers?
RedwoodJS services contain business logic and external API integrations (like MessageBird calls), while GraphQL resolvers are thin layers that call service functions. This separation keeps your code testable and maintainable—services can be reused outside GraphQL contexts.
How do I send SMS to multiple recipients in RedwoodJS?
Modify your GraphQL schema to accept an array of recipients, then loop through them in your service, calling messagebird.messages.create() for each. For bulk sending, consider using MessageBird's batch API or implementing a background job queue (like RedwoodJS jobs with Prisma) to avoid GraphQL timeouts.
Does RedwoodJS support SMS delivery status callbacks from MessageBird?
Yes. Create a webhook endpoint in api/src/functions/ (serverless function) to receive MessageBird delivery status callbacks. Parse the webhook payload, update your database via Prisma, and optionally trigger frontend notifications through GraphQL subscriptions or polling.
Next Steps
Now that you have a working SMS integration in RedwoodJS using MessageBird, you can:
Expand Functionality: Add more SMS features like two-way communication, OTP verification, and appointment reminders.
Production Deployment: Deploy your RedwoodJS application to a production environment (Vercel, Netlify, AWS, etc.) and monitor SMS usage and costs.
User Notifications: Integrate SMS notifications into your user authentication flow, account management, and transactional processes.
Testing: Write comprehensive tests for your SMS service layer and GraphQL mutations to ensure reliability and maintainability.
Frequently asked questions
You can send SMS messages with RedwoodJS by integrating the MessageBird SMS API. Create a RedwoodJS service and a GraphQL mutation to programmatically send messages, leveraging Redwood's full-stack capabilities and MessageBird's reliable platform. This is ideal for sending transactional notifications like OTPs and alerts.
The RedwoodJS MessageBird integration allows you to send SMS messages directly from your RedwoodJS application backend. It uses the MessageBird Node.js SDK and environment variables for API key management. This enables sending transactional SMS notifications like one-time passwords (OTPs) and alerts directly from your web application.
MessageBird offers a reliable and developer-friendly SMS API, and RedwoodJS provides a robust full-stack framework with GraphQL, services, and deployment options. Combining these technologies simplifies sending transactional SMS notifications from your application backend.
First, install the MessageBird Node.js SDK using yarn workspace api add messagebird. Then, set your MessageBird Live API Access Key in a .env file at your project's root, ensuring this file is in .gitignore. Remember to configure the same environment variable in your hosting provider's settings during deployment.
Use the RedwoodJS CLI command yarn rw g service sms to generate the necessary files: sms.ts, sms.scenarios.ts, and sms.test.ts. Implement the SMS sending logic within the generated sms.ts file, utilizing the MessageBird SDK and environment variables.
Use a try-catch block within your RedwoodJS service function and check for errors in the MessageBird SDK callback. Map known MessageBird error codes to RedwoodJS's UserInputError for clearer GraphQL error responses. For unexpected errors, throw a generic Error or a FatalServerError for logging and handling in the client.
GraphQL acts as the API layer in RedwoodJS. Define your sendSms mutation in the SDL (schema definition language) specifying the input parameters and return type. RedwoodJS will automatically map this mutation to your SMS service function. Be sure to protect your mutation with appropriate authorization like @requireAuth.
Use the MessageBird Test API key during initial development to verify integration without sending real SMS or incurring costs. Switch to the Live API key when testing with real phone numbers and in production to send actual messages.
Store your MessageBird API keys securely in environment variables, specifically within a .env file in your project's root directory during development. Add this file to your .gitignore to prevent accidental commits. When deploying, set the same environment variable in your hosting provider's settings.
MessageBird automatically splits long messages exceeding the standard SMS limit (160 characters for GSM, 70 for Unicode). The service includes a length warning. Always use E.164 formatting for recipient numbers to ensure international compatibility.
Yes, MessageBird supports sending to multiple recipients (up to 50) in a single API call. Modify your RedwoodJS service to accept an array of recipients for the messagebird.messages.create method. This is more efficient than sending many individual API calls.
Monitor SMS logs, delivery rates, and costs through the MessageBird Dashboard. Leverage RedwoodJS's logging capabilities for monitoring application-side behavior and integrate with error tracking services for monitoring application health.
Refer to the MessageBird API documentation and error codes. Common issues include authentication failures (check API key), invalid recipient or originator formats (check E.164 format), and insufficient balance. Consult RedwoodJS logs and MessageBird's dashboard for detailed error information.
Robust input validation, especially for phone numbers, is essential to prevent errors, ensure deliverability, and avoid unnecessary costs. Use a dedicated library like libphonenumber-js for comprehensive validation.
Use background jobs for non-time-critical SMS messages, such as bulk notifications or scheduled messages, to avoid blocking main web requests. Implement a queue system to manage these jobs and handle retries for transient errors.
Learn how to integrate MessageBird's SMS API into your RedwoodJS application using GraphQL and the service layer. This comprehensive guide walks you through building SMS functionality with proper E.164 phone validation, error handling, and production-ready architecture.
You'll implement a complete SMS service using RedwoodJS's backend architecture—perfect for sending OTP codes, transactional alerts, appointment reminders, and user notifications through MessageBird's reliable delivery platform.
Why RedwoodJS for SMS Integration?
RedwoodJS provides an opinionated full-stack architecture that simplifies SMS integration:
.envsupport for secure API key storage and configuration across development, staging, and production environmentsPrerequisites
Before you begin this integration, ensure you have the following:
yarn create redwoodjs-app my-app.MessageBird SDK Version Note: This guide uses
messagebirdSDK v4.0.1 (the official Node.js SDK last updated in 2022). While the package is 3+ years old, it remains stable and functional for SMS operations. MessageBird has not deprecated this SDK, and it's still actively used in production environments. However, be aware that maintenance updates are infrequent.1. Project Setup
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
Create RedwoodJS Project: Open your terminal and run:
Navigate to Project Directory:
cd redwood-messagebird-smsInstall MessageBird SDK: Add the official MessageBird Node.js SDK to the API side of your project:
This command specifically installs the package within the
apiworkspace, where our backend logic resides.Create a
.envfile in the root of your project:touch .envAdd your MessageBird Live API Access Key to the
.envfile. You can find this key in your MessageBird Dashboard under Developers -> API access.Replace
YOUR_LIVE_ACCESS_KEYwith your actual key.Important: Ensure
.envis listed in your project's root.gitignorefile (RedwoodJS includes this by default) to prevent accidentally committing your secret key.Test vs. Live Keys: MessageBird provides both Live and Test API keys.
This completes the basic project setup and configuration.
2. Implementing Core SMS Sending Functionality
RedwoodJS uses a service layer (
api/src/services) for business logic. We'll create ansmsservice to handle interactions with the MessageBird API.Generate the SMS Service: Use the RedwoodJS CLI generator:
This creates:
api/src/services/sms/sms.ts: The service file where our logic will live.api/src/services/sms/sms.scenarios.ts: For defining seed data for tests.api/src/services/sms/sms.test.ts: The unit test file.Implement the Sending Logic: Open
api/src/services/sms/sms.tsand replace its contents with the following:// api/src/services/sms/sms.ts import { initClient } from 'messagebird' import type { Messages } from 'messagebird/types' // Import specific types import { logger } from 'src/lib/logger' // Redwood's built-in logger import { UserInputError } from '@redwoodjs/graphql-server' // Redwood's standard error class // Define the expected input structure for clarity and type safety interface SendSmsInput { originator: string recipient: string body: string } // Define the expected success response structure interface SmsSendSuccessResponse { success: boolean messageId: string | null // MessageBird returns an ID on success status: string | null // e.g., 'sent', 'buffered' } // Initialize the MessageBird client using the API key from environment variables // Ensure MESSAGEBIRD_ACCESS_KEY is set in your .env file const accessKey = process.env.MESSAGEBIRD_ACCESS_KEY if (!accessKey) { // Log error instead of throwing immediately during initialization phase // The error will be caught during the actual sendSms call if key is missing logger.error('MESSAGEBIRD_ACCESS_KEY environment variable not set.') // Or, depending on desired behavior: // throw new Error('MESSAGEBIRD_ACCESS_KEY environment variable not set.') } // Initialize client - it might still work even if key is missing temporarily // but API calls will fail later. Handle missing key in the function. const messagebird = initClient(accessKey || 'dummy-key-if-needed') /** * Sends an SMS message using the MessageBird API. * * @param originator - The sender ID (phone number or alphanumeric string). * @param recipient - The recipient's phone number in E.164 format (e.g., +14155552671). * @param body - The text content of the SMS message. * @returns Promise resolving to SmsSendSuccessResponse on success. * @throws UserInputError for validation errors or MessageBird API errors. */ export const sendSms = async ({ originator, recipient, body, }: SendSmsInput): Promise<SmsSendSuccessResponse> => { // Check for Access Key availability here, inside the function call if (!accessKey) { logger.error('MessageBird API Key (MESSAGEBIRD_ACCESS_KEY) is missing.'); throw new Error('SMS Service is not configured. Missing API Key.'); // Use generic Error for config issues } logger.info( { recipient: recipient, originator: originator }, // Avoid logging full body potentially 'Attempting to send SMS via MessageBird' ) // Basic Input Validation (Add more robust validation as needed) if (!originator || !recipient || !body) { throw new UserInputError('Originator, recipient, and body are required.') } // Very basic E.164 format check (improves robustness) if (!/^\+[1-9]\d{1,14}$/.test(recipient)) { throw new UserInputError('Recipient phone number must be in E.164 format (e.g., +14155552671).') } if (body.length > 1600) { // Generous limit, but prevents huge payloads logger.warn('SMS body exceeds typical limits, may be truncated or split.') } const params: Messages.Params = { originator: originator, recipients: [recipient], // API expects an array body: body, } try { const response = await new Promise<Messages.Message>((resolve, reject) => { messagebird.messages.create(params, (err, response) => { if (err) { logger.error({ err }, 'MessageBird API error') // Map MessageBird errors to user-friendly errors let errorMessage = 'Failed to send SMS due to an external service error.' if (err.errors && err.errors.length > 0) { // Example: Extracting specific MessageBird error descriptions errorMessage = `MessageBird Error: ${err.errors[0].description}` if (err.errors[0].code === 2) { // Example: Authentication error errorMessage = 'MessageBird authentication failed. Check API Key.' } else if (err.errors[0].code === 21) { // Example: Bad request (often invalid number) errorMessage = 'Invalid recipient number or originator format.' } } return reject(new UserInputError(errorMessage)) } // Ensure response and recipients info exist before accessing if (response && response.recipients && response.recipients.items && response.recipients.items.length > 0) { resolve(response) } else { logger.error({ response }, 'Unexpected response structure from MessageBird') // Use generic Error for unexpected API responses reject(new Error('Invalid response received from MessageBird API.')) } }) }) // Assuming the first recipient item contains the relevant status/id for single sends. // This is generally safe for single-recipient messages.create calls. const firstRecipientInfo = response.recipients.items[0]; logger.info( { messageId: response.id, status: firstRecipientInfo.status }, 'SMS sent successfully via MessageBird' ) return { success: true, messageId: response.id || null, // Main message ID status: firstRecipientInfo.status || null // Status for the specific recipient } } catch (error) { logger.error({ error }, 'Failed to send SMS') // Re-throw UserInputErrors directly, wrap others if necessary if (error instanceof UserInputError) { throw error; } // Wrap unexpected errors throw new Error(`An unexpected error occurred while sending SMS: ${error.message}`) } }Explanation:
initClientfrommessagebirdand necessary types.loggerfor structured logging andUserInputErrorfor standard GraphQL errors.SendSmsInput,SmsSendSuccessResponse) for better type safety and code clarity.process.env.MESSAGEBIRD_ACCESS_KEYand initialize the client. We now check for the key inside thesendSmsfunction to provide a better error if it's missing during an actual attempt.sendSmsfunction accepts an object withoriginator,recipient, andbody.libphonenumber-jsis recommended for production.paramsobject required bymessagebird.messages.create. Note thatrecipientsmust be an array.messagebird.messages.createmethod is asynchronous and uses a callback pattern. We wrap it in aPromisefor cleanerasync/awaitsyntax.errexists, we log the detailed error and reject the promise with aUserInputError, providing a more user-friendly message where possible by inspectingerr.errors. MessageBird error codes (like2for auth or21for bad request) can be used for more specific feedback. Unexpected API response structures are handled with a genericError.try...catchhandles any unexpected errors during the promise execution or validation.3. Building the API Layer (GraphQL Mutation)
RedwoodJS automatically maps service functions to GraphQL resolvers. We need to define the GraphQL schema (SDL) for our
sendSmsmutation.Generate the SDL: Use the RedwoodJS CLI generator:
This creates
api/src/graphql/sms.sdl.ts.Define the Mutation Schema: Open
api/src/graphql/sms.sdl.tsand replace its contents with the following:# api/src/graphql/sms.sdl.ts export const schema = gql` """""" Input parameters for sending an SMS message. """""" input SendSmsInput { ""The sender ID (phone number or alphanumeric string)."" originator: String! ""The recipient's phone number in E.164 format (e.g., +14155552671)."" recipient: String! ""The text content of the SMS message (max ~1600 chars recommended)."" body: String! } """""" Response object after attempting to send an SMS. Note: Errors are typically handled via the top-level 'errors' field in the GraphQL response. """""" type SmsSendResponse { ""Indicates whether the API call to MessageBird was initiated successfully by the service."" success: Boolean! ""The unique ID assigned to the message by MessageBird (if successful)."" messageId: String ""The status reported by MessageBird for the recipient (e.g., 'sent', 'buffered')."" status: String } """""" Mutations related to sending SMS messages. """""" type Mutation { """""" Sends a single SMS message via MessageBird. Requires originator, recipient (E.164 format), and body. Throws errors for invalid input or API failures. """""" sendSms(input: SendSmsInput!): SmsSendResponse! @skipAuth # Or use @requireAuth } `Explanation:
input SendSmsInputtype mirroring the structure expected by our service function.type SmsSendResponsefor the data returned by the mutation upon successful initiation, including success status, the MessageBird message ID, and the initial status.error: Stringfield has been removed. GraphQL standard practice is to handle errors through the top-levelerrorsarray in the response. RedwoodJS automatically populates this when a service function throws anError(especiallyUserInputError). The client should check for the presence of thiserrorsarray.sendSmsmutation within theMutationtype. It accepts theSendSmsInputand returnsSmsSendResponseon success.@skipAuth: For simplicity in this guide, we're skipping authentication. In a real application, you MUST protect this mutation using@requireAuthor similar directives to ensure only authorized users/systems can send SMS.RedwoodJS automatically connects this SDL definition to the
sendSmsfunction inapi/src/services/sms/sms.tsbased on naming conventions.4. Integrating with MessageBird (API Keys Recap)
While covered in setup, this section recaps the key aspects of MessageBird API key usage:
Obtain API Key:
Secure Storage & Purpose:
MESSAGEBIRD_ACCESS_KEYenvironment variable (in.envlocally) is used by thesmsservice (process.env.MESSAGEBIRD_ACCESS_KEY) to authenticate requests.Environment Handling:
.envfile is used during development (yarn rw dev).MESSAGEBIRD_ACCESS_KEY) in your hosting provider's settings (e.g., Vercel, Netlify Environment Variables). Do not commit your.envfile.5. Error Handling, Logging, and Retries
Our service implementation includes basic error handling and logging.
UserInputErrorfor clearer feedback via GraphQL (returned in theerrorsarray).try...catchand typically throw a genericErroror potentially Redwood'sFatalServerError.UserInputErrorfor invalid input or predictable API issues (bad number, auth fail).logger(import { logger } from 'src/lib/logger').logger.info: Log successful attempts and outcomes, including the MessageBird message ID and status. Include key parameters like recipient/originator for context (avoid logging sensitive body content unless necessary and compliant).logger.error: Log detailed error objects (errfrom MessageBird,errorfrom catch blocks) to aid debugging. Include context like input parameters where safe.6. Database Schema and Data Layer (Optional)
For this basic guide focused purely on sending an SMS, a database schema isn't strictly required. However, in a production application, you would likely want to log SMS sending attempts and their outcomes.
Potential Schema (Example using Prisma):
Implementation:
schema.prisma.yarn rw prisma migrate devto apply changes.sendSmsservice function:SmsLogentry with status 'attempted'.messageBirdId.errorMessage.This database logging is omitted from the main code for simplicity but is a recommended practice.
7. Security Features
Protecting your SMS sending functionality is crucial.
@skipAuthwith@requireAuth(or role-based directives) on thesendSmsmutation inapi/src/graphql/sms.sdl.ts. This ensures only logged-in/authorized users or systems can trigger the SMS send. Refer to RedwoodJS Authentication documentation.libphonenumber-jsfor robust phone number validation and formatting.body.originatoragainst allowed values (your purchased numbers or registered alphanumeric IDs)..envand never committing the file or exposing the key in frontend code.sendSmsmutation. Options:rate-limiter-flexiblewith Redis or an in-memory store.sendSmsservice (e.g., querySmsLogtimestamps per user/recipient).8. Handling Special Cases
+14155552671) as the originator.+followed by country code and number, no spaces/dashes). Our basic validation hints at this. Use robust validation (e.g.,libphonenumber-js).9. Performance Optimizations
For single transactional SMS, application performance is usually not the bottleneck.
recipientsarray. Modify the service if batch sending is needed. Avoid many sequential individual API calls.10. Monitoring, Observability, and Analytics
/graphql/healthendpoint or create custom checks.11. Troubleshooting and Caveats
Authentication failed(Code 2): CheckMESSAGEBIRD_ACCESS_KEY.recipient is invalid/originator is invalid(Code 21): Check E.164 format for recipient; check originator validity (VMN format or registered Alphanumeric ID per country rules).No balance(Code 9): Add MessageBird credits.Message body is empty: Ensurebodyis provided.401 Unauthorized: API key issue.400 Bad Request: Invalid parameters; check MessageBird error details in logs.sent) doesn't guarantee final delivery. Use MessageBird webhooks for DLRs (delivered,failed).12. Deployment and CI/CD
MESSAGEBIRD_ACCESS_KEYwith your Live key in your hosting provider's environment variable settings. Ensure it's available to the API service at runtime (and potentially build time).yarn rw deploy vercel).MESSAGEBIRD_ACCESS_KEYsecret. Include tests (yarn rw test api).13. Verification and Testing
Run
yarn rw dev.Use the GraphQL playground (
http://localhost:8911/graphql) to execute thesendSmsmutation:mutation SendTestSms { sendSms(input: { originator: ""+1YOUR_MESSAGEBIRD_NUMBER"" # Or valid Alphanumeric ID recipient: ""+1RECIPIENT_PHONE_NUMBER"" # Your test phone number body: ""Hello from RedwoodJS and MessageBird!"" }) { success messageId status } }Replace placeholders. Check the GraphQL response (expect data, not errors). Check API logs. Check the phone (if using Live key). Check MessageBird Dashboard Log.
Automated Testing (Unit/Integration):
api/src/services/sms/sms.test.ts.// api/src/services/sms/sms.test.ts import { sendSms } from './sms' import { initClient } from 'messagebird' // Import to mock import { UserInputError } from '@redwoodjs/graphql-server' // Mock the MessageBird SDK type MockMessageBirdClient = { messages: { create: jest.Mock } } // Hold the mock implementation details let mockMessagesCreate: jest.Mock; jest.mock('messagebird', () => { mockMessagesCreate = jest.fn((params, callback) => { // Default success mock implementation callback(null, { id: 'mock-message-id-123', recipients: { totalCount: 1, totalSentCount: 1, totalDeliveredCount: 0, totalDeliveryFailedCount: 0, items: [{ recipient: params.recipients[0], status: 'sent', statusDatetime: new Date().toISOString(), messagePartCount: 1, }] } }) }); return { initClient: jest.fn().mockImplementation((accessKey) => { // Basic check if access key is provided during init simulation if (!accessKey || accessKey === 'dummy-key-if-needed') { // Simulate client init even without key, but create will fail if key needed } return { // Return the mocked client structure messages: { create: mockMessagesCreate, }, } }), } }) describe('sms service', () => { const OLD_ENV = process.env; beforeEach(() => { // Reset mocks and environment variables before each test jest.clearAllMocks(); // Ensure the env var is set for most tests process.env = { ...OLD_ENV, MESSAGEBIRD_ACCESS_KEY: 'test-key-is-set' }; // Reset mock implementation to default success mockMessagesCreate.mockImplementation((params, callback) => { callback(null, { id: 'mock-message-id-123', recipients: { items: [{ status: 'sent' }] } }); }); }); afterAll(() => { process.env = OLD_ENV; // Restore old environment }); it('sends an SMS successfully', async () => { const input = { originator: '+15550001111', recipient: '+15552223333', body: 'Test message', } const result = await sendSms(input) expect(mockMessagesCreate).toHaveBeenCalledTimes(1) expect(mockMessagesCreate).toHaveBeenCalledWith( { originator: input.originator, recipients: [input.recipient], body: input.body, }, expect.any(Function) // Callback function ) expect(result).toEqual({ success: true, messageId: 'mock-message-id-123', status: 'sent', // From the mocked response }) }) it('throws UserInputError for missing parameters', async () => { const input = { originator: '+15550001111', recipient: '', body: 'Test' }; await expect(sendSms(input)).rejects.toThrow(UserInputError); await expect(sendSms(input)).rejects.toThrow('Originator, recipient, and body are required.'); }) it('throws UserInputError for invalid recipient format', async () => { const input = { originator: '+15550001111', recipient: 'invalid-number', body: 'Test' }; await expect(sendSms(input)).rejects.toThrow(UserInputError); await expect(sendSms(input)).rejects.toThrow('Recipient phone number must be in E.164 format'); }) })How the RedwoodJS SMS Integration Works
Let's break down the key components:
GraphQL SDL (Schema Definition Language)
The GraphQL SDL (Schema Definition Language) defines the structure of our GraphQL API. In this case, we have a
sendSmsmutation that accepts aSendSmsInputand returns aSmsSendResponse.Service Implementation
The service implementation in
api/src/services/sms/sms.tsis where the actual MessageBird API calls are made. It validates input, constructs the request, and handles the response from the MessageBird API.E.164 Phone Number Format
MessageBird requires international phone numbers in E.164 format:
+[country code][subscriber number]without spaces or special characters.E.164 Format Rules:
+followed by 1-3 digit country code (e.g.,+1for USA/Canada,+44for UK)+sign+14155552671(US),+442071234567(UK),+8613800138000(China)Our service validates E.164 format using regex:
/^\+[1-9]\d{1,14}$/to catch malformed numbers before making expensive API calls.SMS Character Limits and Encoding
Understanding SMS character limits prevents message truncation and unexpected costs:
MessageBird automatically handles message segmentation. If your message exceeds these limits, it will be split into multiple segments, each billed separately. Plan your message content accordingly.
Common Character Limit Scenarios:
Deploying Your RedwoodJS SMS Integration to Production
When deploying your SMS integration to production:
Environment Variables
Ensure your MessageBird API key is securely stored in your production environment. Use RedwoodJS's
.envfile for development and configure your hosting provider's environment variables for production.Error Handling and Logging
Implement robust error handling and logging to monitor SMS delivery and troubleshoot issues. Use RedwoodJS's
loggerfor structured logging and integrate with monitoring tools like Sentry for error tracking.Rate Limiting and Cost Management
SMS providers charge per message sent. Protect your application from abuse and unexpected costs:
express-rate-limitor implement custom rate limiting in your GraphQL context to restrict SMS sends per user/IPsendSmsmutation using RedwoodJS's@requireAuthdirective to prevent anonymous abuseVersion Compatibility
Troubleshooting MessageBird SMS Integration Issues
"SMS Service is not configured"
Cause: The
MESSAGEBIRD_ACCESS_KEYenvironment variable is missing or not loaded.Solution:
.envcontainsMESSAGEBIRD_ACCESS_KEY=your_live_or_test_key_hereyarn rw dev) to reload environment variables.envis in your project root (same directory asredwood.toml)"Recipient phone number must be in E.164 format"
Cause: The phone number doesn't match the E.164 format requirements.
Solution:
+followed by the country code+1 (415) 555-2671→+14155552671libphonenumber-jsfor automatic formatting and validation"MessageBird authentication failed"
Cause: Invalid or expired API access key.
Solution:
.envfile:MESSAGEBIRD_ACCESS_KEY=sk_live_...(no quotes needed)"Invalid recipient number or originator format"
Cause: The recipient phone number is invalid for the destination country, or the originator is not properly registered.
Solution:
Frequently Asked Questions
How do I send SMS messages from a RedwoodJS application?
Install the MessageBird SDK (
yarn workspace api add messagebird), create a GraphQL SDL schema defining asendSmsmutation, and implement a service inapi/src/services/sms/sms.tsthat calls MessageBird's API. Use RedwoodJS environment variables to store your MessageBird API key securely.What phone number format does MessageBird require?
MessageBird requires E.164 international format:
+[country code][subscriber number]with no spaces or special characters. Examples:+14155552671(US),+442071234567(UK). Validate using the regex pattern/^\+[1-9]\d{1,14}$/.How do I handle MessageBird API errors in RedwoodJS?
Wrap MessageBird API calls in try-catch blocks or Promise rejection handlers. Map MessageBird error codes (like code 2 for authentication failures, code 21 for invalid numbers) to user-friendly error messages using RedwoodJS's
UserInputErrororForbiddenErrorclasses.Can I use MessageBird test keys in RedwoodJS development?
Yes. MessageBird provides test API keys (prefix
test_) that simulate SMS sending without actually delivering messages or charging your account. SetMESSAGEBIRD_ACCESS_KEY=test_...in your.envfile for development and testing.How do I prevent SMS abuse in my RedwoodJS application?
Implement rate limiting using middleware, require user authentication with RedwoodJS's
@requireAuthdirective, enforce per-user daily SMS caps by tracking sends in your Prisma database, and monitor MessageBird's dashboard for unusual activity.What's the difference between RedwoodJS services and GraphQL resolvers?
RedwoodJS services contain business logic and external API integrations (like MessageBird calls), while GraphQL resolvers are thin layers that call service functions. This separation keeps your code testable and maintainable—services can be reused outside GraphQL contexts.
How do I send SMS to multiple recipients in RedwoodJS?
Modify your GraphQL schema to accept an array of recipients, then loop through them in your service, calling
messagebird.messages.create()for each. For bulk sending, consider using MessageBird's batch API or implementing a background job queue (like RedwoodJS jobs with Prisma) to avoid GraphQL timeouts.Does RedwoodJS support SMS delivery status callbacks from MessageBird?
Yes. Create a webhook endpoint in
api/src/functions/(serverless function) to receive MessageBird delivery status callbacks. Parse the webhook payload, update your database via Prisma, and optionally trigger frontend notifications through GraphQL subscriptions or polling.Next Steps
Now that you have a working SMS integration in RedwoodJS using MessageBird, you can: