How to Send MMS with MessageBird in RedwoodJS: Complete Tutorial (2025)
Learn how to send MMS messages with MessageBird in RedwoodJS. Step-by-step guide to building GraphQL mutations for multimedia messaging with images, videos, and audio. Includes webhook setup, delivery tracking, and production security.
Learn how to integrate MessageBird's MMS API into your RedwoodJS application to send multimedia messages programmatically. This comprehensive guide walks you through building a production-ready MMS service with GraphQL mutations, media attachment handling, delivery status tracking, and webhook verification.
You'll build a complete MMS messaging solution using RedwoodJS's service layer architecture, Prisma ORM for database tracking, and the official MessageBird Node.js SDK. By the end, you'll have a fully functional system capable of sending MMS messages with images, videos, and audio files to US and Canadian phone numbers.
Target Audience: RedwoodJS developers and Node.js engineers implementing programmatic MMS messaging for notifications, marketing campaigns, or customer communication.
Prerequisites:
Node.js 20+ (required for RedwoodJS 8.x as of January 2025)
Yarn package manager
A RedwoodJS project (version 8.8 or later recommended). If you don't have one, create it: yarn create redwood-app ./my-mms-app
A MessageBird account with API access
A MessageBird Access Key (Live or Test)
An MMS-enabled virtual mobile number purchased from MessageBird (currently limited to US/Canada for sending, as of January 2025)
Supported media formats: Audio (audio/basic, audio/mp4, audio/mpeg, audio/3gpp, audio/amr-nb, audio/amr, etc.), Video (video/mpeg, video/mp4, video/quicktime, video/3gpp, video/H264, etc.), Image (image/jpeg, image/gif, image/png, image/bmp), Text (text/vcard, text/csv, text/rtf), Application (application/pdf)
Maximum file size: 1 MB (1024 KB) per individual media attachment; total MMS size should be ≤900 KB across all attachments for guaranteed cross-carrier delivery in US/Canada
Maximum media attachments: 10 files per MMS message
Media URL requirements: Publicly accessible URLs that respond within 5 seconds
Character limits: Subject up to 256 characters, body up to 2000 characters
Geographic availability: US and Canada only for MMS sending (January 2025)
System Architecture:
Text
[User] -> [Redwood Web Frontend] -(GraphQL Mutation)-> [Redwood API Backend]
|
v
[MMS Service (api/src/services/mms)]
| - Initializes MessageBird Client
| - Calls MessageBird MMS API
| - Interacts with Database (Prisma)
v
[MessageBird API] -> [Carrier Network] -> [Recipient]
^
| (Status Webhook with JWT Signature)
|
[Webhook Handler (api/src/functions/messagebirdWebhook)] <- [MessageBird API]
1. Project Setup and Configuration
Before sending your first MMS message, you'll need to install the MessageBird SDK and configure your RedwoodJS environment with API credentials.
Navigate to your project directory:
Bash
cd my-mms-app
Install the MessageBird Node.js SDK:
Install the SDK specifically in the api workspace.
Bash
yarn workspace api add messagebird
Package Version (January 2025):messagebird SDK (latest v4.0.1) supports Node.js >= 0.10, but RedwoodJS 8.x requires Node 20+. Ensure your environment uses Node 20 or higher.
Configure Environment Variables:
RedwoodJS uses .env files for environment variables. Create or open the .env file in the root of your project and add your MessageBird credentials and sender ID.
.env
# .env
MESSAGEBIRD_ACCESS_KEY=YOUR_MESSAGEBIRD_API_ACCESS_KEY
# Use an MMS-enabled number purchased from MessageBird (E.164 format)
MESSAGEBIRD_ORIGINATOR=+1XXXXXXXXXX
# Webhook signature verification key (optional but recommended)
MESSAGEBIRD_SIGNING_KEY=YOUR_SIGNING_KEY_FROM_DASHBOARD
MESSAGEBIRD_ACCESS_KEY: Your API access key from the MessageBird Dashboard (Developers → API access). Treat this like a password and keep it secret. Use a live key for actual sending or a test key for development without incurring charges or sending real messages.
MESSAGEBIRD_ORIGINATOR: The MMS-enabled phone number in E.164 format (e.g., +12025550187). This number must be associated with your MessageBird account and enabled for MMS. US/Canada numbers only support MMS sending (as of January 2025).
MESSAGEBIRD_SIGNING_KEY: Your signing key for webhook signature verification. Find this in the MessageBird Dashboard under Developers → Settings. This ensures webhook requests are authentic.
Ensure Environment Variables are Loaded: RedwoodJS automatically loads variables from .env into process.env. No further action is needed for them to be accessible in the API side.
2. Database Schema for Message Tracking
Tracking MMS delivery status is essential for production applications. Store message details in your database using Prisma to enable delivery tracking, debugging, and message history queries.
Define the Prisma Schema:
Open api/db/schema.prisma and add the following model:
Prisma
// api/db/schema.prisma
model MmsMessage {
id String @id @default(cuid())
messageBirdId String? @unique // The ID returned by MessageBird API
recipient String // Recipient phone number (E.164 format)
originator String // Sending number/ID
subject String?
body String?
mediaUrls String[] // Store URLs as an array of strings
status String @default("pending") // e.g., pending, sent, delivered, failed, received
statusMessage String? // Store error messages or details
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([messageBirdId]) // Index for faster webhook lookups
@@index([status]) // Index for status queries
@@index([recipient]) // Index for recipient-based queries
}
messageBirdId: Stores the unique ID returned by MessageBird upon successful sending, allowing you to correlate updates later. Marked as optional initially as it's only available after a successful API call.
mediaUrls: Prisma supports string arrays, suitable for storing the list of media URLs.
status: Tracks the message lifecycle. Initialized as pending.
Indexes: Added for performance optimization on common query patterns (webhook lookups, status filtering, recipient searches).
Apply Schema Changes (Database Migration):
Run the following command to create and apply a new database migration:
Bash
yarn rw prisma migrate dev
Follow the prompts to name your migration (e.g., add_mms_message_model).
3. Building the GraphQL API Layer
Create a GraphQL mutation to expose MMS sending functionality to your frontend application. This mutation will accept recipient details, message content, and media URLs.
Generate GraphQL and Service Files:
Use the Redwood generator to scaffold the necessary files for an mms resource.
Bash
yarn rw g sdl mms --crud
This command creates:
api/src/graphql/mms.sdl.ts: Defines the GraphQL schema types and operations.
api/src/services/mms/mms.ts: Contains the business logic (service functions).
api/src/services/mms/mms.scenarios.ts: For database seeding during tests.
api/src/services/mms/mms.test.ts: For writing unit tests.
Define the sendMms Mutation:
Modify api/src/graphql/mms.sdl.ts to define a specific mutation for sending MMS. Remove the generated CRUD operations for now and add the sendMms mutation.
SendMmsInput specifies the required data: recipient number, optional subject/body, and an array of media URLs. At least body or mediaUrls must be provided in the actual MessageBird API call.
The sendMms mutation takes this input and returns the MmsMessage record created in the database.
@requireAuth: This directive ensures only authenticated users can call this mutation. Adjust or remove based on your application's auth requirements.
Added Query operations for retrieving messages.
4. Implementing the MMS Service Logic
Implement the core MMS sending logic in your RedwoodJS service layer. This service will handle MessageBird API calls, input validation, error handling, and database updates.
Edit the MMS Service:
Open api/src/services/mms/mms.ts and replace its contents with the following:
TypeScript
// api/src/services/mms/mms.tsimporttype { QueryResolvers, MutationResolvers, SendMmsInput } from'types/graphql'import { validate } from'@redwoodjs/api'import { logger } from'src/lib/logger'import { db } from'src/lib/db'// Import the specific function neededimport { initClient } from'messagebird'// Initialize the MessageBird client// Ensure MESSAGEBIRD_ACCESS_KEY is set in your .env fileconst messagebird = initClient(process.env.MESSAGEBIRD_ACCESS_KEY)
interfaceMessageBirdMmsParams {
originator: stringrecipients: string[]
subject?: stringbody?: stringmediaUrls?: string[]
reference?: string// Optional: Client reference for status reports
}
// MessageBird MMS Error Codes (https://docs.bird.com/api/channels-api/message-status-and-interactions/message-failure-sources/sms-platform-extended-error-codes)constRETRYABLE_ERROR_CODES = [2, 20, 21, 30, 34] // Rate limit, temporary carrier/network issuesconstPERMANENT_ERROR_CODES = [1, 9, 103, 104, 105, 110] // Invalid number, opted out, unregistered sender/contentexportconstsendMms: MutationResolvers['sendMms'] = async ({ input }: { input: SendMmsInput }) => {
// 1. Validate Inputvalidate(input.recipient, 'Recipient', { presence: true })
validate(input.mediaUrls, 'Media URLs', { presence: true }) // MMS requires media// Validate E.164 format for recipientconst e164Regex = /^\+[1-9]\d{1,14}$/if (!e164Regex.test(input.recipient)) {
thrownewError('Recipient must be in E.164 format (e.g., +12025550123)')
}
if (!input.body && (!input.mediaUrls || input.mediaUrls.length === 0)) {
thrownewError('MMS must include either a body or at least one media URL.')
}
if (input.mediaUrls && input.mediaUrls.length > 10) {
// MessageBird limit (verified January 2025)thrownewError('MMS cannot contain more than 10 media attachments.')
}
// Ensure originator is configuredconst originator = process.env.MESSAGEBIRD_ORIGINATORif (!originator) {
logger.error('MESSAGEBIRD_ORIGINATOR environment variable is not set.')
thrownewError('MMS sending is not configured correctly.')
}
// 2. Prepare data for MessageBird APIconstmmsParams: MessageBirdMmsParams = {
originator: originator,
recipients: [input.recipient], // API expects an arraysubject: input.subject,
body: input.body,
mediaUrls: input.mediaUrls,
// Generate a unique reference for trackingreference: `mms_${Date.now()}_${Math.random().toString(36).substring(7)}`
}
let messageBirdResponse = nulllet errorMessage = nulllet initialStatus = 'failed'// Default status// 3. Create initial record in DB (tracks all attempts)const dbRecord = await db.mmsMessage.create({
data: {
recipient: input.recipient,
originator: originator,
subject: input.subject,
body: input.body,
mediaUrls: input.mediaUrls,
status: 'pending', // Start as pending
},
})
try {
// 4. Call MessageBird API
logger.info({ mmsParams }, 'Attempting to send MMS via MessageBird')
messageBirdResponse = awaitnewPromise((resolve, reject) => {
messagebird.mms.create(mmsParams, (err, response) => {
if (err) {
logger.error({ err }, 'MessageBird API error')
reject(err) // Reject the promise on error
} else {
logger.info({ response }, 'MessageBird API success')
resolve(response) // Resolve the promise on success
}
})
})
// 5. Update DB record on Success
initialStatus = 'sent'// Status from MessageBird perspectiveawait db.mmsMessage.update({
where: { id: dbRecord.id },
data: {
messageBirdId: messageBirdResponse.id,
status: initialStatus,
},
})
// Return the updated recordreturn { ...dbRecord, messageBirdId: messageBirdResponse.id, status: initialStatus }
} catch (error) {
// 6. Handle Errors and Update DB record on Failure
logger.error({ error, recipient: input.recipient }, 'Failed to send MMS')
errorMessage = error.message || 'Unknown error during MMS sending.'// Extract specific errors if available (MessageBird SDK error structure)if (error.errors) {
errorMessage = error.errors.map(e =>`${e.description} (Code: ${e.code})`).join(', ')
}
await db.mmsMessage.update({
where: { id: dbRecord.id },
data: {
status: 'failed',
statusMessage: errorMessage,
},
})
// Re-throw error to signal failure to GraphQL layerthrownewError(`Failed to send MMS: ${errorMessage}`)
}
}
// Query resolvers for retrieving messagesexportconstmmsMessage: QueryResolvers['mmsMessage'] = ({ id }) => {
return db.mmsMessage.findUnique({
where: { id },
})
}
exportconstmmsMessages: QueryResolvers['mmsMessages'] = () => {
return db.mmsMessage.findMany({
orderBy: { createdAt: 'desc' },
})
}
Explanation:
Imports: Import necessary types, Redwood functions (validate, logger, db), and initClient from messagebird.
Client Initialization: The messagebird client is initialized outside the function using the MESSAGEBIRD_ACCESS_KEY from environment variables. This reuses the client instance.
Error Code Constants: Added constants for retryable and permanent error codes based on MessageBird documentation.
Input Validation: Enhanced validation includes E.164 format checking using regex, required field checks, and MessageBird limits on media attachments.
Originator Check: Ensures the MESSAGEBIRD_ORIGINATOR is configured.
Prepare Params: Constructs the mmsParams object matching the structure required by messagebird.mms.create. Note that recipients must be an array. Added unique reference ID for tracking.
Initial DB Record: Create a record in the MmsMessage table before calling the API with a pending status. This helps track attempts even if the API call fails immediately.
API Call: Wrap the messagebird.mms.create call in a Promise because the SDK uses a callback pattern. This allows using async/await.
Success Handling: If the API call succeeds, update the database record with the messageBirdId returned by the API and set the status to sent.
Error Handling: A try...catch block catches errors from input validation or the API call promise rejection. Extract meaningful error messages from the caught error object. Update database record with failed status and error message. Re-throw error to signal failure to GraphQL layer.
Query Resolvers: Added functions to retrieve individual or all MMS messages.
5. MessageBird API Integration Details
Understanding how to authenticate and structure API requests ensures reliable MMS delivery. The MessageBird SDK handles most complexity, but proper configuration is essential.
Authentication: Handled automatically by initClient(process.env.MESSAGEBIRD_ACCESS_KEY). Ensure the key is correct and has the necessary permissions in your MessageBird account.
API Endpoint: The SDK directs requests to the correct MessageBird MMS API endpoint (https://rest.messagebird.com/mms).
Parameters: Map SendMmsInput to the required messagebird.mms.create parameters:
originator: From process.env.MESSAGEBIRD_ORIGINATOR.
recipients: Takes input.recipient and puts it in an array [input.recipient].
subject: From input.subject.
body: From input.body.
mediaUrls: From input.mediaUrls. Ensure these are publicly accessible URLs. MessageBird needs to fetch them. They must also adhere to size (1 MB max per file, 900 KB total recommended) and type constraints as documented in MessageBird MMS API.
Secure Credentials: Using .env is the standard way to handle API keys securely in RedwoodJS. Never commit your .env file to version control. Use .env.defaults for non-sensitive defaults and add .env to your .gitignore file.
You can view your Live and Test API keys here. Click the eye icon to reveal and copy the desired key.
To get an MMS-enabled number:
Navigate to Numbers.
Buy a new number, ensuring it's in the US or Canada and supports MMS capabilities.
Copy the number in E.164 format (e.g., +1...).
To get your signing key for webhook verification:
Navigate to Developers → Settings.
Copy your signing key for webhook signature verification.
6. Error Handling and Retry Strategies
Robust error handling prevents message loss and provides actionable debugging information. Understanding MessageBird error codes helps you implement appropriate retry logic.
Error Handling Strategy: Use try...catch to capture exceptions during the process. Errors are logged, and the corresponding database record is updated with a 'failed' status and an error message. The error is then re-thrown to the GraphQL layer.
Logging: Redwood's built-in logger (pino) is used to log informational messages (API call attempts, success) and errors. Check your console output when running yarn rw dev or your production log streams. Logged objects ({ err }, { response }) provide detailed context. MessageBird API error responses often contain an errors array with specific code, description, and parameter fields.
Retry Mechanisms: This implementation does not include automatic retries. For production systems, consider adding retry logic for transient network errors or specific MessageBird error codes (codes 2, 20, 21, 30, 34). Libraries like async-retry can help implement strategies like exponential backoff. This typically involves catching specific error types/codes and scheduling a background job (using RedwoodJS background functions or external queues like Redis/BullMQ) to retry the sendMms operation after a delay.
7. Security Best Practices
Securing your MMS integration prevents unauthorized access, protects API credentials, and ensures compliance with messaging regulations.
Authentication/Authorization: The @requireAuth directive on the sendMms mutation ensures only logged-in users can trigger it. Implement role-based access control if needed (e.g., only admins can send certain types of MMS).
Input Validation: Perform comprehensive validation using @redwoodjs/api's validate function and custom checks:
E.164 phone number format validation using regex
Body/media presence validation
Media count validation (max 10)
URL format validation for media URLs
Input Sanitization: While less critical for phone numbers and pre-signed media URLs, always sanitize user-generated text inputs (subject, body) if they are displayed elsewhere in your application to prevent XSS attacks. Libraries like dompurify (if rendering HTML) or simple replacements can help.
API Key Security: Storing the key in .env and not committing it is paramount. Use secrets management solutions (like Doppler, Vercel environment variables, AWS Secrets Manager) in production environments.
Rate Limiting: MessageBird imposes rate limits on API requests. Implement rate limiting on your GraphQL mutation (e.g., using Redwood's directives or middleware with libraries like graphql-rate-limit-directive) to prevent abuse and hitting MessageBird limits.
Media URL Security: Ensure the mediaUrls provided point to trusted sources. If users upload media, use secure storage (like S3, GCS) and consider pre-signed URLs with limited expiry times passed to the mutation, rather than directly public URLs if possible. Validate file types and sizes during upload.
Webhook Signature Verification: Implement JWT signature verification for webhooks to ensure requests are authentic and haven't been tampered with (see Section 9).
8. Testing Your MMS Integration
Thorough testing validates your implementation before deploying to production. Test both successful sends and error scenarios to ensure reliability.
Unit Testing the Service:
RedwoodJS generates a test file (api/src/services/mms/mms.test.ts). Modify it to test the sendMms logic. Mock the messagebird client and the db client.
Open the GraphQL Playground, usually at http://localhost:8911/graphql.
Ensure your .env file has valid Test or Live credentials and a valid MMS-enabled Originator Number.
Execute the sendMms mutation:
GraphQL
mutation SendTestMms {
sendMms(input:{recipient:"+1RECIPIENTNUMBER", # Use a real number you can checksubject:"Redwood Test MMS",
body:"Hello from RedwoodJS!",
mediaUrls:["https://developers.messagebird.com/img/logos/mb-400.jpg"]}){
id
messageBirdId
recipient
status
statusMessage
mediaUrls
subject
body
}}
Checklist:
Did the mutation complete without errors in the Playground?
Check the API server console (yarn rw dev output) for logs. Are there errors?
Check your database (e.g., using yarn rw prisma studio) – was an MmsMessage record created? Does it have a messageBirdId and sent status (if successful) or failed status and an error message?
If using a Live key and number, did the recipient phone receive the MMS message with the subject, body, and image? (This might take a few seconds to minutes).
If using a Test key, the message won't actually be delivered, but the API call should succeed or fail according to MessageBird's test environment rules, and the DB record should reflect this.
9. Webhook Setup for Delivery Status Updates
Receive real-time delivery status updates from MessageBird using webhooks. Proper JWT signature verification ensures webhook authenticity and prevents spoofing attacks.
Create a Webhook Handler Function:
Use the Redwood generator to create an HTTP function.
Bash
yarn rw g function messagebirdWebhook --typescript
Implement the Handler with JWT Signature Verification:
Edit api/src/functions/messagebirdWebhook.ts. MessageBird sends status updates via GET requests with a JWT signature header per MessageBird API documentation.
TypeScript
// api/src/functions/messagebirdWebhook.tsimporttype { APIGatewayEvent, Context } from'aws-lambda'import { logger } from'src/lib/logger'import { db } from'src/lib/db'// Import MessageBird webhook signature verificationconst { RequestValidator } = require('messagebird/lib/webhook-signature-jwt')
/**
* Handles incoming status report webhooks from MessageBird for MMS/SMS.
* MessageBird sends status updates as GET requests with JWT signature.
* See: https://developers.messagebird.com/api
*/exportconsthandler = async (event: APIGatewayEvent, _context: Context) => {
logger.info({ method: event.httpMethod }, 'Received request on messagebirdWebhook')
// --- Security Check: Verify JWT Signature ---const signingKey = process.env.MESSAGEBIRD_SIGNING_KEYif (!signingKey) {
logger.error('MESSAGEBIRD_SIGNING_KEY not configured')
return { statusCode: 500, body: 'Webhook signing key not configured' }
}
try {
// Verify the signature using MessageBird's SDKconst validator = newRequestValidator(signingKey)
// Reconstruct the full URL for validationconst protocol = event.headers['X-Forwarded-Proto'] || 'https'const host = event.headers.Host || event.headers.hostconst path = event.requestContext?.path || event.pathconst queryString = event.queryStringParameters
? '?' + newURLSearchParams(event.queryStringParameters).toString()
: ''const fullUrl = `${protocol}://${host}${path}${queryString}`const signature = event.headers['MessageBird-Signature-JWT'] ||
event.headers['messagebird-signature-jwt']
if (!signature) {
logger.warn('Missing MessageBird-Signature-JWT header')
return { statusCode: 401, body: 'Missing signature' }
}
// Validate the signature (throws error if invalid)const requestBody = event.body || ''const isValid = validator.validateSignature(signature, fullUrl, requestBody)
if (!isValid) {
logger.warn('Invalid webhook signature')
return { statusCode: 401, body: 'Invalid signature' }
}
logger.info('Webhook signature verified successfully')
} catch (error) {
logger.error({ error }, 'Webhook signature verification failed')
return { statusCode: 401, body: 'Signature verification failed' }
}
if (event.httpMethod === 'GET') {
// Handle Status Report (GET request)const params = event.queryStringParameters
logger.info({ params }, 'Processing MessageBird GET Status Report')
if (!params || !params.id || !params.status || !params.recipient || !params.statusDatetime) {
logger.warn('Webhook GET request missing required parameters.')
return { statusCode: 400, body: 'Missing parameters' }
}
const messageBirdId = params.idconst status = params.status// e.g., 'delivered', 'delivery_failed', 'sent', 'buffered'const recipient = params.recipientconst statusDatetime = params.statusDatetimetry {
const updatedRecord = await db.mmsMessage.updateMany({
where: {
messageBirdId: messageBirdId,
},
data: {
status: status,
statusMessage: `Status updated via webhook at ${statusDatetime}`,
updatedAt: newDate(statusDatetime),
},
})
if (updatedRecord.count === 0) {
logger.warn({ messageBirdId }, 'No matching MmsMessage found in DB for status update.')
return { statusCode: 200, body: 'OK (No matching record)' }
}
logger.info({ messageBirdId, status }, 'Successfully updated MmsMessage status from webhook.')
return { statusCode: 200, body: 'OK' }
} catch (error) {
logger.error({ error, messageBirdId }, 'Error updating MmsMessage status from webhook.')
return { statusCode: 500, body: 'Internal Server Error' }
}
} elseif (event.httpMethod === 'POST') {
// Handle Incoming Message (MO - Mobile Originated)
logger.info('Received POST request (potentially incoming message)')
// Implement logic here if needed for receiving MMSreturn { statusCode: 200, body: 'OK (POST received)' }
} else {
logger.warn(`Unsupported HTTP method: ${event.httpMethod}`)
return { statusCode: 405, body: 'Method Not Allowed' }
}
}
Key Security Enhancements:
JWT Signature Verification: Uses MessageBird's RequestValidator to verify the MessageBird-Signature-JWT header per MessageBird webhook documentation. This ensures requests are authentic and haven't been tampered with.
Signing Key: Requires MESSAGEBIRD_SIGNING_KEY from environment variables (obtained from MessageBird Dashboard → Developers → Settings).
URL Reconstruction: Properly reconstructs the full URL including protocol, host, path, and query string for signature validation.
Early Return on Failure: Returns 401 Unauthorized if signature verification fails, preventing processing of potentially malicious requests.
Configure Webhook in MessageBird Dashboard:
Navigate to Developers → Webhooks in your MessageBird Dashboard.
Create a new webhook for MMS status updates.
Set the URL to your deployed function endpoint (e.g., https://your-domain.com/.redwood/functions/messagebirdWebhook).
Select the events you want to receive (delivery reports, incoming messages).
MessageBird will automatically sign all webhook requests with your signing key.
Testing Webhooks Locally:
Use a service like ngrok to expose your local development server:
Bash
ngrok http 8911
Copy the HTTPS URL provided by ngrok and configure it as your webhook URL in MessageBird Dashboard (append /.redwood/functions/messagebirdWebhook to the ngrok URL).
10. Deploying to Production
Prepare your MMS integration for production with proper environment configuration, monitoring, and security measures.
Environment Variables: Set all required environment variables (MESSAGEBIRD_ACCESS_KEY, MESSAGEBIRD_ORIGINATOR, MESSAGEBIRD_SIGNING_KEY) in your hosting platform (Vercel, Netlify, AWS, etc.).
Database Migration: Run Prisma migrations in production:
Bash
yarn rw prisma migrate deploy
HTTPS Required: MessageBird webhooks require HTTPS endpoints. Ensure your production deployment uses SSL/TLS certificates.
Rate Limiting: Implement application-level rate limiting to prevent abuse and stay within MessageBird's API limits. US/Canada numbers have a daily limit of 500 SMS per day per number as documented in MessageBird number restrictions.
Monitoring and Logging: Set up proper logging and monitoring for production. Use services like Datadog, Sentry, or LogRocket to track errors and performance.
Webhook Endpoint Security: Ensure your webhook endpoint:
Verifies JWT signatures (as implemented above)
Returns appropriate HTTP status codes (200 for success, 401 for auth failures, 500 for internal errors)
Handles retries gracefully (MessageBird retries failed webhooks up to 10 times with increasing intervals)
Background Job Queue: For high-volume MMS sending, consider implementing a background job queue using RedwoodJS background jobs or external systems like BullMQ/Redis to avoid timeouts and improve reliability.
Cost Management: Monitor MMS costs. Pricing varies by country and carrier. Check Bird SMS/MMS pricing for current rates (typically $0.0075-$0.015 per SMS segment in the US, MMS costs more).
Summary: Your Complete MMS Integration
Congratulations! You've built a production-ready MessageBird MMS integration for RedwoodJS with:
GraphQL mutation for sending MMS with media attachments
Prisma database schema for tracking message status
Comprehensive error handling with MessageBird error codes
Secure webhook handling with JWT signature verification
Unit tests for service logic
E.164 phone number validation
Production-ready security considerations
Next Steps:
Implement retry logic for failed messages using error code classification
Add background job processing for high-volume sending
Build a frontend UI for composing and tracking MMS messages
Set up monitoring and alerting for delivery failures
Implement user authentication and role-based access control
Add media upload functionality with pre-signed URLs
Integrate MessageBird's MMS API into your RedwoodJS application by installing the MessageBird SDK, configuring environment variables with your access key and originator number, and creating a GraphQL mutation to trigger the sending process. This allows you to send multimedia messages like notifications and marketing content directly from your RedwoodJS project.
The MessageBird originator is the MMS-enabled phone number or approved alphanumeric sender ID used to send MMS messages. It must be in E.164 format (e.g., +12025550187) and associated with your MessageBird account. This is configured using the MESSAGEBIRD_ORIGINATOR environment variable in your RedwoodJS project.
While not strictly required for sending, a database helps track message details (recipient, status, media URLs, errors) for debugging, history, and potential future features. The Prisma schema defines an MmsMessage model to store these details, enabling efficient management and logging.
The example creates a database record with a 'pending' status before calling the MessageBird API. This approach allows tracking attempts even if the API call fails immediately. You can choose to create the record after successful API calls if preferred, but pre-creation aids in comprehensive logging.
MMS sending with MessageBird via a virtual mobile number is currently limited to the US and Canada for the number originator. Check the MessageBird documentation for updates and potential use of Alphanumeric Sender IDs. Ensure your recipient numbers are in the correct international format.
Navigate to your RedwoodJS project directory and run yarn workspace api add messagebird. This installs the necessary SDK within the API side of your project, allowing you to interact with the MessageBird API for sending MMS messages.
Prisma is used for database interactions, specifically for creating and updating records in the MmsMessage table. This table stores the details of each MMS message, including recipient, status, media URLs, and any error messages encountered during the sending process. Prisma simplifies database operations within your RedwoodJS application.
Set up a webhook handler function in your RedwoodJS API to receive status updates from MessageBird. MessageBird sends these updates as GET requests to your configured URL, containing parameters like message ID, status, and recipient. You can then update the corresponding record in your database to reflect the current message status.
Store your MessageBird access key as an environment variable in a .env file located in the root of your RedwoodJS project. Ensure that this file is added to your .gitignore to prevent it from being committed to version control, protecting your API credentials.
MessageBird's MMS API has a limit of 10 media attachments per message. The provided service implementation includes validation to prevent exceeding this limit. Ensure your attached media files are publicly accessible URLs and adhere to MessageBird's size and format requirements.
The @requireAuth directive on the sendMms mutation ensures only logged-in users can initiate MMS sending. This basic access control prevents unauthorized access and can be customized with role-based access control (RBAC) if needed. Remove or adjust it if your application does not require authentication.
RedwoodJS generates a test file where you can mock the MessageBird client and the database client using Jest. This allows you to simulate successful and failed API calls and verify the database interaction and error handling logic without actually sending MMS messages. Use yarn rw test api to run tests.
The provided service implementation uses a try...catch block to handle errors during API calls. Errors are logged with details using Redwood's logger, the database record is updated with a 'failed' status and the error message, and then the error is re-thrown for the GraphQL layer to process. Consider implementing retry mechanisms with exponential backoff for transient errors.
Use environment variables for API keys, implement input validation and sanitization to protect against common web vulnerabilities, use pre-signed URLs with limited expiry for media attachments, and add rate limiting to your GraphQL mutation to prevent misuse and stay within MessageBird's limits.
Learn how to integrate MessageBird's MMS API into your RedwoodJS application to send multimedia messages programmatically. This comprehensive guide walks you through building a production-ready MMS service with GraphQL mutations, media attachment handling, delivery status tracking, and webhook verification.
You'll build a complete MMS messaging solution using RedwoodJS's service layer architecture, Prisma ORM for database tracking, and the official MessageBird Node.js SDK. By the end, you'll have a fully functional system capable of sending MMS messages with images, videos, and audio files to US and Canadian phone numbers.
Target Audience: RedwoodJS developers and Node.js engineers implementing programmatic MMS messaging for notifications, marketing campaigns, or customer communication.
Prerequisites:
yarn create redwood-app ./my-mms-appMMS Technical Specifications (MessageBird MMS API, Bird MMS Limits):
System Architecture:
1. Project Setup and Configuration
Before sending your first MMS message, you'll need to install the MessageBird SDK and configure your RedwoodJS environment with API credentials.
Navigate to your project directory:
cd my-mms-appInstall the MessageBird Node.js SDK: Install the SDK specifically in the
apiworkspace.Package Version (January 2025): messagebird SDK (latest v4.0.1) supports Node.js >= 0.10, but RedwoodJS 8.x requires Node 20+. Ensure your environment uses Node 20 or higher.
Configure Environment Variables: RedwoodJS uses
.envfiles for environment variables. Create or open the.envfile in the root of your project and add your MessageBird credentials and sender ID.MESSAGEBIRD_ACCESS_KEY: Your API access key from the MessageBird Dashboard (Developers → API access). Treat this like a password and keep it secret. Use a live key for actual sending or a test key for development without incurring charges or sending real messages.MESSAGEBIRD_ORIGINATOR: The MMS-enabled phone number in E.164 format (e.g.,+12025550187). This number must be associated with your MessageBird account and enabled for MMS. US/Canada numbers only support MMS sending (as of January 2025).MESSAGEBIRD_SIGNING_KEY: Your signing key for webhook signature verification. Find this in the MessageBird Dashboard under Developers → Settings. This ensures webhook requests are authentic.Ensure Environment Variables are Loaded: RedwoodJS automatically loads variables from
.envintoprocess.env. No further action is needed for them to be accessible in the API side.2. Database Schema for Message Tracking
Tracking MMS delivery status is essential for production applications. Store message details in your database using Prisma to enable delivery tracking, debugging, and message history queries.
Define the Prisma Schema: Open
api/db/schema.prismaand add the following model:messageBirdId: Stores the unique ID returned by MessageBird upon successful sending, allowing you to correlate updates later. Marked as optional initially as it's only available after a successful API call.mediaUrls: Prisma supports string arrays, suitable for storing the list of media URLs.status: Tracks the message lifecycle. Initialized aspending.Apply Schema Changes (Database Migration): Run the following command to create and apply a new database migration:
Follow the prompts to name your migration (e.g.,
add_mms_message_model).3. Building the GraphQL API Layer
Create a GraphQL mutation to expose MMS sending functionality to your frontend application. This mutation will accept recipient details, message content, and media URLs.
Generate GraphQL and Service Files: Use the Redwood generator to scaffold the necessary files for an
mmsresource.This command creates:
api/src/graphql/mms.sdl.ts: Defines the GraphQL schema types and operations.api/src/services/mms/mms.ts: Contains the business logic (service functions).api/src/services/mms/mms.scenarios.ts: For database seeding during tests.api/src/services/mms/mms.test.ts: For writing unit tests.Define the
sendMmsMutation: Modifyapi/src/graphql/mms.sdl.tsto define a specific mutation for sending MMS. Remove the generated CRUD operations for now and add thesendMmsmutation.# api/src/graphql/mms.sdl.ts export const schema = gql` type MmsMessage { id: String! messageBirdId: String recipient: String! originator: String! subject: String body: String mediaUrls: [String!]! status: String! statusMessage: String createdAt: DateTime! updatedAt: DateTime! } input SendMmsInput { recipient: String! subject: String body: String mediaUrls: [String!]! } type Mutation { sendMms(input: SendMmsInput!): MmsMessage! @requireAuth } type Query { mmsMessage(id: String!): MmsMessage @requireAuth mmsMessages: [MmsMessage!]! @requireAuth } `MmsMessagetype mirrors the Prisma model.SendMmsInputspecifies the required data: recipient number, optional subject/body, and an array of media URLs. At leastbodyormediaUrlsmust be provided in the actual MessageBird API call.sendMmsmutation takes this input and returns theMmsMessagerecord created in the database.@requireAuth: This directive ensures only authenticated users can call this mutation. Adjust or remove based on your application's auth requirements.4. Implementing the MMS Service Logic
Implement the core MMS sending logic in your RedwoodJS service layer. This service will handle MessageBird API calls, input validation, error handling, and database updates.
Edit the MMS Service: Open
api/src/services/mms/mms.tsand replace its contents with the following:// api/src/services/mms/mms.ts import type { QueryResolvers, MutationResolvers, SendMmsInput } from 'types/graphql' import { validate } from '@redwoodjs/api' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Import the specific function needed import { initClient } from 'messagebird' // Initialize the MessageBird client // Ensure MESSAGEBIRD_ACCESS_KEY is set in your .env file const messagebird = initClient(process.env.MESSAGEBIRD_ACCESS_KEY) interface MessageBirdMmsParams { originator: string recipients: string[] subject?: string body?: string mediaUrls?: string[] reference?: string // Optional: Client reference for status reports } // MessageBird MMS Error Codes (https://docs.bird.com/api/channels-api/message-status-and-interactions/message-failure-sources/sms-platform-extended-error-codes) const RETRYABLE_ERROR_CODES = [2, 20, 21, 30, 34] // Rate limit, temporary carrier/network issues const PERMANENT_ERROR_CODES = [1, 9, 103, 104, 105, 110] // Invalid number, opted out, unregistered sender/content export const sendMms: MutationResolvers['sendMms'] = async ({ input }: { input: SendMmsInput }) => { // 1. Validate Input validate(input.recipient, 'Recipient', { presence: true }) validate(input.mediaUrls, 'Media URLs', { presence: true }) // MMS requires media // Validate E.164 format for recipient const e164Regex = /^\+[1-9]\d{1,14}$/ if (!e164Regex.test(input.recipient)) { throw new Error('Recipient must be in E.164 format (e.g., +12025550123)') } if (!input.body && (!input.mediaUrls || input.mediaUrls.length === 0)) { throw new Error('MMS must include either a body or at least one media URL.') } if (input.mediaUrls && input.mediaUrls.length > 10) { // MessageBird limit (verified January 2025) throw new Error('MMS cannot contain more than 10 media attachments.') } // Ensure originator is configured const originator = process.env.MESSAGEBIRD_ORIGINATOR if (!originator) { logger.error('MESSAGEBIRD_ORIGINATOR environment variable is not set.') throw new Error('MMS sending is not configured correctly.') } // 2. Prepare data for MessageBird API const mmsParams: MessageBirdMmsParams = { originator: originator, recipients: [input.recipient], // API expects an array subject: input.subject, body: input.body, mediaUrls: input.mediaUrls, // Generate a unique reference for tracking reference: `mms_${Date.now()}_${Math.random().toString(36).substring(7)}` } let messageBirdResponse = null let errorMessage = null let initialStatus = 'failed' // Default status // 3. Create initial record in DB (tracks all attempts) const dbRecord = await db.mmsMessage.create({ data: { recipient: input.recipient, originator: originator, subject: input.subject, body: input.body, mediaUrls: input.mediaUrls, status: 'pending', // Start as pending }, }) try { // 4. Call MessageBird API logger.info({ mmsParams }, 'Attempting to send MMS via MessageBird') messageBirdResponse = await new Promise((resolve, reject) => { messagebird.mms.create(mmsParams, (err, response) => { if (err) { logger.error({ err }, 'MessageBird API error') reject(err) // Reject the promise on error } else { logger.info({ response }, 'MessageBird API success') resolve(response) // Resolve the promise on success } }) }) // 5. Update DB record on Success initialStatus = 'sent' // Status from MessageBird perspective await db.mmsMessage.update({ where: { id: dbRecord.id }, data: { messageBirdId: messageBirdResponse.id, status: initialStatus, }, }) // Return the updated record return { ...dbRecord, messageBirdId: messageBirdResponse.id, status: initialStatus } } catch (error) { // 6. Handle Errors and Update DB record on Failure logger.error({ error, recipient: input.recipient }, 'Failed to send MMS') errorMessage = error.message || 'Unknown error during MMS sending.' // Extract specific errors if available (MessageBird SDK error structure) if (error.errors) { errorMessage = error.errors.map(e => `${e.description} (Code: ${e.code})`).join(', ') } await db.mmsMessage.update({ where: { id: dbRecord.id }, data: { status: 'failed', statusMessage: errorMessage, }, }) // Re-throw error to signal failure to GraphQL layer throw new Error(`Failed to send MMS: ${errorMessage}`) } } // Query resolvers for retrieving messages export const mmsMessage: QueryResolvers['mmsMessage'] = ({ id }) => { return db.mmsMessage.findUnique({ where: { id }, }) } export const mmsMessages: QueryResolvers['mmsMessages'] = () => { return db.mmsMessage.findMany({ orderBy: { createdAt: 'desc' }, }) }Explanation:
validate,logger,db), andinitClientfrommessagebird.messagebirdclient is initialized outside the function using theMESSAGEBIRD_ACCESS_KEYfrom environment variables. This reuses the client instance.MESSAGEBIRD_ORIGINATORis configured.mmsParamsobject matching the structure required bymessagebird.mms.create. Note thatrecipientsmust be an array. Added unique reference ID for tracking.MmsMessagetable before calling the API with apendingstatus. This helps track attempts even if the API call fails immediately.messagebird.mms.createcall in aPromisebecause the SDK uses a callback pattern. This allows usingasync/await.messageBirdIdreturned by the API and set the status tosent.try...catchblock catches errors from input validation or the API call promise rejection. Extract meaningful error messages from the caughterrorobject. Update database record withfailedstatus and error message. Re-throw error to signal failure to GraphQL layer.5. MessageBird API Integration Details
Understanding how to authenticate and structure API requests ensures reliable MMS delivery. The MessageBird SDK handles most complexity, but proper configuration is essential.
initClient(process.env.MESSAGEBIRD_ACCESS_KEY). Ensure the key is correct and has the necessary permissions in your MessageBird account.https://rest.messagebird.com/mms).SendMmsInputto the requiredmessagebird.mms.createparameters:originator: Fromprocess.env.MESSAGEBIRD_ORIGINATOR.recipients: Takesinput.recipientand puts it in an array[input.recipient].subject: Frominput.subject.body: Frominput.body.mediaUrls: Frominput.mediaUrls. Ensure these are publicly accessible URLs. MessageBird needs to fetch them. They must also adhere to size (1 MB max per file, 900 KB total recommended) and type constraints as documented in MessageBird MMS API..envis the standard way to handle API keys securely in RedwoodJS. Never commit your.envfile to version control. Use.env.defaultsfor non-sensitive defaults and add.envto your.gitignorefile.Obtaining MessageBird Credentials:
+1...).6. Error Handling and Retry Strategies
Robust error handling prevents message loss and provides actionable debugging information. Understanding MessageBird error codes helps you implement appropriate retry logic.
try...catchto capture exceptions during the process. Errors are logged, and the corresponding database record is updated with a 'failed' status and an error message. The error is then re-thrown to the GraphQL layer.logger(pino) is used to log informational messages (API call attempts, success) and errors. Check your console output when runningyarn rw devor your production log streams. Logged objects ({ err },{ response }) provide detailed context. MessageBird API error responses often contain anerrorsarray with specificcode,description, andparameterfields.async-retrycan help implement strategies like exponential backoff. This typically involves catching specific error types/codes and scheduling a background job (using RedwoodJS background functions or external queues like Redis/BullMQ) to retry thesendMmsoperation after a delay.7. Security Best Practices
Securing your MMS integration prevents unauthorized access, protects API credentials, and ensures compliance with messaging regulations.
@requireAuthdirective on thesendMmsmutation ensures only logged-in users can trigger it. Implement role-based access control if needed (e.g., only admins can send certain types of MMS).@redwoodjs/api'svalidatefunction and custom checks:subject,body) if they are displayed elsewhere in your application to prevent XSS attacks. Libraries likedompurify(if rendering HTML) or simple replacements can help..envand not committing it is paramount. Use secrets management solutions (like Doppler, Vercel environment variables, AWS Secrets Manager) in production environments.graphql-rate-limit-directive) to prevent abuse and hitting MessageBird limits.mediaUrlsprovided point to trusted sources. If users upload media, use secure storage (like S3, GCS) and consider pre-signed URLs with limited expiry times passed to the mutation, rather than directly public URLs if possible. Validate file types and sizes during upload.8. Testing Your MMS Integration
Thorough testing validates your implementation before deploying to production. Test both successful sends and error scenarios to ensure reliability.
Unit Testing the Service: RedwoodJS generates a test file (
api/src/services/mms/mms.test.ts). Modify it to test thesendMmslogic. Mock themessagebirdclient and thedbclient.// api/src/services/mms/mms.test.ts import { sendMms } from './mms' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' // Mock the MessageBird SDK let mockMmsCreate jest.mock('messagebird', () => { mockMmsCreate = jest.fn() return { initClient: jest.fn().mockReturnValue({ mms: { create: mockMmsCreate, }, }), } }) // Mock the logger to prevent console noise during tests jest.mock('src/lib/logger') describe('mms service', () => { // Mock database interactions const mockDbMmsMessage = { create: jest.fn(), update: jest.fn(), } beforeAll(() => { // Assign the mock implementation to db.mmsMessage Object.assign(db, { mmsMessage: mockDbMmsMessage }) // Set required environment variables for tests process.env.MESSAGEBIRD_ORIGINATOR = '+15550001111' process.env.MESSAGEBIRD_ACCESS_KEY = 'test_key' }) afterEach(() => { // Reset mocks after each test jest.clearAllMocks() mockMmsCreate.mockReset() }) it('sends an MMS successfully', async () => { const input = { recipient: '+15551112222', subject: 'Test Subject', body: 'Test Body', mediaUrls: ['http://example.com/image.jpg'], } const mockDbRecord = { id: 'mock-db-id-1', recipient: input.recipient, originator: process.env.MESSAGEBIRD_ORIGINATOR, subject: input.subject, body: input.body, mediaUrls: input.mediaUrls, status: 'pending', messageBirdId: null, createdAt: new Date(), updatedAt: new Date(), } const mockApiResponse = { id: 'mb-mms-id-123', } // Mock DB create response mockDbMmsMessage.create.mockResolvedValue(mockDbRecord) // Mock MessageBird API success via callback mockMmsCreate.mockImplementation((params, callback) => { callback(null, mockApiResponse) }) const result = await sendMms({ input }) // Assertions expect(mockDbMmsMessage.create).toHaveBeenCalledTimes(1) expect(mockDbMmsMessage.create).toHaveBeenCalledWith({ data: expect.objectContaining({ recipient: input.recipient, status: 'pending', mediaUrls: input.mediaUrls, }) }) expect(mockMmsCreate).toHaveBeenCalledTimes(1) expect(mockMmsCreate).toHaveBeenCalledWith( expect.objectContaining({ recipients: [input.recipient], originator: process.env.MESSAGEBIRD_ORIGINATOR, body: input.body, mediaUrls: input.mediaUrls, }), expect.any(Function) ) expect(mockDbMmsMessage.update).toHaveBeenCalledTimes(1) expect(mockDbMmsMessage.update).toHaveBeenCalledWith({ where: { id: mockDbRecord.id }, data: expect.objectContaining({ messageBirdId: mockApiResponse.id, status: 'sent', }), }) expect(result).toEqual(expect.objectContaining({ id: mockDbRecord.id, messageBirdId: mockApiResponse.id, status: 'sent', recipient: input.recipient, })) expect(logger.error).not.toHaveBeenCalled() }) it('handles MessageBird API error', async () => { const input = { recipient: '+15553334444', mediaUrls: ['http://example.com/image.png'], } const mockDbRecord = { id: 'mock-db-id-2', status: 'pending' } mockDbMmsMessage.create.mockResolvedValue(mockDbRecord) const mockApiError = { errors: [{ code: 21, description: 'Recipient is invalid', parameter: 'recipients' }], } // Mock MessageBird API failure via callback mockMmsCreate.mockImplementation((params, callback) => { callback(mockApiError, null) }) // Expect the function to throw an error await expect(sendMms({ input })).rejects.toThrow( /Failed to send MMS: Recipient is invalid \(Code: 21\)/ ) expect(mockDbMmsMessage.create).toHaveBeenCalledTimes(1) expect(mockMmsCreate).toHaveBeenCalledTimes(1) expect(mockDbMmsMessage.update).toHaveBeenCalledWith({ where: { id: mockDbRecord.id }, data: expect.objectContaining({ status: 'failed', statusMessage: 'Recipient is invalid (Code: 21)', }), }) expect(logger.error).toHaveBeenCalled() }) it('throws error if originator is not configured', async () => { const originalOriginator = process.env.MESSAGEBIRD_ORIGINATOR delete process.env.MESSAGEBIRD_ORIGINATOR const input = { recipient: '+15551112222', mediaUrls: ['url'] } await expect(sendMms({ input })).rejects.toThrow( 'MMS sending is not configured correctly.' ) expect(mockDbMmsMessage.create).not.toHaveBeenCalled() expect(mockMmsCreate).not.toHaveBeenCalled() process.env.MESSAGEBIRD_ORIGINATOR = originalOriginator }) it('validates E.164 phone number format', async () => { const input = { recipient: '555-1234', mediaUrls: ['url'] } await expect(sendMms({ input })).rejects.toThrow( 'Recipient must be in E.164 format' ) expect(mockDbMmsMessage.create).not.toHaveBeenCalled() expect(mockMmsCreate).not.toHaveBeenCalled() }) })Run tests using
yarn rw test api.Manual Verification (Development):
Start the development server:
yarn rw dev.Open the GraphQL Playground, usually at
http://localhost:8911/graphql.Ensure your
.envfile has valid Test or Live credentials and a valid MMS-enabled Originator Number.Execute the
sendMmsmutation:mutation SendTestMms { sendMms(input: { recipient: "+1RECIPIENTNUMBER", # Use a real number you can check subject: "Redwood Test MMS", body: "Hello from RedwoodJS!", mediaUrls: ["https://developers.messagebird.com/img/logos/mb-400.jpg"] }) { id messageBirdId recipient status statusMessage mediaUrls subject body } }Checklist:
yarn rw devoutput) for logs. Are there errors?yarn rw prisma studio) – was anMmsMessagerecord created? Does it have amessageBirdIdandsentstatus (if successful) orfailedstatus and an error message?9. Webhook Setup for Delivery Status Updates
Receive real-time delivery status updates from MessageBird using webhooks. Proper JWT signature verification ensures webhook authenticity and prevents spoofing attacks.
Create a Webhook Handler Function: Use the Redwood generator to create an HTTP function.
yarn rw g function messagebirdWebhook --typescriptImplement the Handler with JWT Signature Verification: Edit
api/src/functions/messagebirdWebhook.ts. MessageBird sends status updates via GET requests with a JWT signature header per MessageBird API documentation.// api/src/functions/messagebirdWebhook.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Import MessageBird webhook signature verification const { RequestValidator } = require('messagebird/lib/webhook-signature-jwt') /** * Handles incoming status report webhooks from MessageBird for MMS/SMS. * MessageBird sends status updates as GET requests with JWT signature. * See: https://developers.messagebird.com/api */ export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info({ method: event.httpMethod }, 'Received request on messagebirdWebhook') // --- Security Check: Verify JWT Signature --- const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY if (!signingKey) { logger.error('MESSAGEBIRD_SIGNING_KEY not configured') return { statusCode: 500, body: 'Webhook signing key not configured' } } try { // Verify the signature using MessageBird's SDK const validator = new RequestValidator(signingKey) // Reconstruct the full URL for validation const protocol = event.headers['X-Forwarded-Proto'] || 'https' const host = event.headers.Host || event.headers.host const path = event.requestContext?.path || event.path const queryString = event.queryStringParameters ? '?' + new URLSearchParams(event.queryStringParameters).toString() : '' const fullUrl = `${protocol}://${host}${path}${queryString}` const signature = event.headers['MessageBird-Signature-JWT'] || event.headers['messagebird-signature-jwt'] if (!signature) { logger.warn('Missing MessageBird-Signature-JWT header') return { statusCode: 401, body: 'Missing signature' } } // Validate the signature (throws error if invalid) const requestBody = event.body || '' const isValid = validator.validateSignature(signature, fullUrl, requestBody) if (!isValid) { logger.warn('Invalid webhook signature') return { statusCode: 401, body: 'Invalid signature' } } logger.info('Webhook signature verified successfully') } catch (error) { logger.error({ error }, 'Webhook signature verification failed') return { statusCode: 401, body: 'Signature verification failed' } } if (event.httpMethod === 'GET') { // Handle Status Report (GET request) const params = event.queryStringParameters logger.info({ params }, 'Processing MessageBird GET Status Report') if (!params || !params.id || !params.status || !params.recipient || !params.statusDatetime) { logger.warn('Webhook GET request missing required parameters.') return { statusCode: 400, body: 'Missing parameters' } } const messageBirdId = params.id const status = params.status // e.g., 'delivered', 'delivery_failed', 'sent', 'buffered' const recipient = params.recipient const statusDatetime = params.statusDatetime try { const updatedRecord = await db.mmsMessage.updateMany({ where: { messageBirdId: messageBirdId, }, data: { status: status, statusMessage: `Status updated via webhook at ${statusDatetime}`, updatedAt: new Date(statusDatetime), }, }) if (updatedRecord.count === 0) { logger.warn({ messageBirdId }, 'No matching MmsMessage found in DB for status update.') return { statusCode: 200, body: 'OK (No matching record)' } } logger.info({ messageBirdId, status }, 'Successfully updated MmsMessage status from webhook.') return { statusCode: 200, body: 'OK' } } catch (error) { logger.error({ error, messageBirdId }, 'Error updating MmsMessage status from webhook.') return { statusCode: 500, body: 'Internal Server Error' } } } else if (event.httpMethod === 'POST') { // Handle Incoming Message (MO - Mobile Originated) logger.info('Received POST request (potentially incoming message)') // Implement logic here if needed for receiving MMS return { statusCode: 200, body: 'OK (POST received)' } } else { logger.warn(`Unsupported HTTP method: ${event.httpMethod}`) return { statusCode: 405, body: 'Method Not Allowed' } } }Key Security Enhancements:
RequestValidatorto verify theMessageBird-Signature-JWTheader per MessageBird webhook documentation. This ensures requests are authentic and haven't been tampered with.MESSAGEBIRD_SIGNING_KEYfrom environment variables (obtained from MessageBird Dashboard → Developers → Settings).Configure Webhook in MessageBird Dashboard:
https://your-domain.com/.redwood/functions/messagebirdWebhook).Testing Webhooks Locally: Use a service like ngrok to expose your local development server:
Copy the HTTPS URL provided by ngrok and configure it as your webhook URL in MessageBird Dashboard (append
/.redwood/functions/messagebirdWebhookto the ngrok URL).10. Deploying to Production
Prepare your MMS integration for production with proper environment configuration, monitoring, and security measures.
Environment Variables: Set all required environment variables (
MESSAGEBIRD_ACCESS_KEY,MESSAGEBIRD_ORIGINATOR,MESSAGEBIRD_SIGNING_KEY) in your hosting platform (Vercel, Netlify, AWS, etc.).Database Migration: Run Prisma migrations in production:
HTTPS Required: MessageBird webhooks require HTTPS endpoints. Ensure your production deployment uses SSL/TLS certificates.
Rate Limiting: Implement application-level rate limiting to prevent abuse and stay within MessageBird's API limits. US/Canada numbers have a daily limit of 500 SMS per day per number as documented in MessageBird number restrictions.
Monitoring and Logging: Set up proper logging and monitoring for production. Use services like Datadog, Sentry, or LogRocket to track errors and performance.
Webhook Endpoint Security: Ensure your webhook endpoint:
Background Job Queue: For high-volume MMS sending, consider implementing a background job queue using RedwoodJS background jobs or external systems like BullMQ/Redis to avoid timeouts and improve reliability.
Cost Management: Monitor MMS costs. Pricing varies by country and carrier. Check Bird SMS/MMS pricing for current rates (typically $0.0075-$0.015 per SMS segment in the US, MMS costs more).
Summary: Your Complete MMS Integration
Congratulations! You've built a production-ready MessageBird MMS integration for RedwoodJS with:
Next Steps:
Related Guides:
External Resources: