This guide provides a step-by-step walkthrough for integrating the MessageBird SMS API into a RedwoodJS application to enable basic outbound SMS functionality. We'll build a simple RedwoodJS service and GraphQL mutation to send SMS messages programmatically.
This setup solves the common need to send transactional SMS notifications (like OTPs, alerts, or confirmations) directly from your web application backend. We use RedwoodJS for its full-stack capabilities and integrated tooling (GraphQL API, services, deployment options) and MessageBird for its reliable SMS delivery platform and developer-friendly API.
System Architecture:
+-----------------+ +-----------------------+ +---------------------+ +-----------------+ +------------+
| Redwood |----->| Redwood API (GraphQL) |----->| Redwood SMS Service |----->| MessageBird SDK |----->| MessageBird|
| Frontend (User) | | (localhost:8911/gql) | | (api/src/services) | | (Node.js) | | API |
+-----------------+ +-----------------------+ +---------------------+ +-----------------+ +------------+
| | Mutation Trigger | Calls MessageBird | Wraps HTTP Call | Sends SMS
| | (`sendSms`) | `messages.create` | |
+---------------------+---------------------------+------------------------+------------------------+------------+
|
v
+--------------+
| Phone Number |
+--------------+
Prerequisites:
- Node.js (LTS version recommended)
- Yarn v1 (Recommended by RedwoodJS, check current docs for updates)
- RedwoodJS CLI (
yarn global add redwoodjs/cli
) - A MessageBird account (Sign up at MessageBird.com)
- A verified phone number or purchased virtual number in MessageBird to use as an originator.
- Your MessageBird Live API Access Key.
By the end of this guide, you will have a RedwoodJS application with a GraphQL mutation capable of sending an SMS message via MessageBird.
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:
yarn create redwood-app ./redwood-messagebird-sms
-
Navigate to Project Directory:
cd redwood-messagebird-sms
-
Install MessageBird SDK: Add the official MessageBird Node.js SDK to the API side of your project:
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: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.# .env MESSAGEBIRD_ACCESS_KEY=YOUR_LIVE_ACCESS_KEY
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.
-
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:
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:// 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:
- We import
initClient
frommessagebird
and necessary types. - We import Redwood's
logger
for structured logging andUserInputError
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 thesendSms
function to provide a better error if it's missing during an actual attempt. - The
sendSms
function accepts an object withoriginator
,recipient
, andbody
. - 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 bymessagebird.messages.create
. Note thatrecipients
must be an array. - The
messagebird.messages.create
method is asynchronous and uses a callback pattern. We wrap it in aPromise
for cleanerasync/await
syntax. - Error Handling: Inside the callback, if
err
exists, 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 (like2
for auth or21
for bad request) can be used for more specific feedback. Unexpected API response structures are handled with a genericError
. - 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.
- We import
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:
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:# 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:
- 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-levelerrors
array in the response. RedwoodJS automatically populates this when a service function throws anError
(especiallyUserInputError
). The client should check for the presence of thiserrors
array. - We define the
sendSms
mutation within theMutation
type. It accepts theSendSmsInput
and returnsSmsSendResponse
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.
- We define an
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 thesms
service (process.env.MESSAGEBIRD_ACCESS_KEY
) to authenticate requests. - The key is a long alphanumeric string provided by MessageBird.
# .env (Example) MESSAGEBIRD_ACCESS_KEY=YOUR_COPIED_KEY
- The key stored in the
-
Environment Handling:
- 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.
- Your local
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 theerrors
array). - Catch unexpected errors (e.g., network issues, invalid SDK response) in the outer
try...catch
and typically throw a genericError
or potentially Redwood'sFatalServerError
. - 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).
- Use Redwood's built-in
- 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):
// 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.
- Before calling MessageBird, create an
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 thesendSms
mutation 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.
- Protect the Mutation: As mentioned in Section 3, replace
- 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).
- Use a library like
- Sanitization: Standard string handling is usually sufficient for SMS bodies. Ensure no unexpected characters could break the MessageBird API call.
- Service Layer: Our service performs basic checks (required fields, E.164 format hint). Enhance this:
- 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.
- Handled via
- 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., querySmsLog
timestamps per user/recipient).
- RedwoodJS Directives: Create a custom directive using packages like
- 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.
- Originator Rules:
- Alphanumeric Sender IDs: (e.g., ""MyCompany"") Max 11 characters. Not supported everywhere (e.g., US requires 10DLC/Toll-Free). Check MessageBird Country Restrictions and Sender ID Availability.
- 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
).
- Always use the E.164 format for recipient numbers (
- 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.
11. Troubleshooting and Caveats
- Common Errors:
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
: Ensurebody
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 thesendSms
mutation: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):
- Use Jest mocking for the MessageBird SDK in
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'); }) })
- Use Jest mocking for the MessageBird SDK in