code examples
code examples
Send MMS with Infobip and RedwoodJS: Complete Developer Guide
A step-by-step guide on integrating Infobip MMS into a RedwoodJS application, covering backend services, API endpoints, database logging, and webhook handling.
How to Send MMS Messages with Infobip in RedwoodJS
This guide provides a step-by-step walkthrough for integrating Infobip's MMS capabilities into a RedwoodJS application. You'll build a backend service and API endpoint to send multimedia messages (images, text) via Infobip, including setup, implementation, error handling, security, and deployment considerations.
By the end of this tutorial, you'll have a RedwoodJS application capable of sending MMS messages programmatically using Infobip, complete with database tracking and delivery status updates via webhooks. This enables richer communication features within your application, such as sending image notifications, personalized visual content, or user-uploaded media confirmations.
Project Overview and Goals
What You'll Build:
- A RedwoodJS backend service to interact with the Infobip MMS API
- A GraphQL mutation to trigger sending MMS messages securely
- Database persistence to log sent messages and track their status
- A webhook handler to receive and process delivery reports from Infobip
Problem Solved:
This implementation provides a reliable and scalable way to send MMS messages from your RedwoodJS application, abstracting the complexities of the Infobip API and integrating it smoothly into the RedwoodJS workflow.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. Its structure simplifies API creation, database interaction, and service implementation
- Infobip: A global cloud communications platform providing APIs for SMS, MMS, voice, email, and more. You'll use their MMS API
- Node.js: The underlying runtime for RedwoodJS
- Prisma: The default ORM in RedwoodJS, used for database modeling and access
- GraphQL: Used by RedwoodJS for API layer communication
- Axios: A promise-based HTTP client for making requests to the Infobip API
System Architecture:
The system architecture involves the client interacting with the RedwoodJS frontend, which triggers a GraphQL mutation in the RedwoodJS API. This API calls a dedicated Infobip MMS service, which communicates with the external Infobip API. The Infobip API sends the message through the carrier network to the end user's mobile device. The Infobip service also logs message details to the RedwoodJS database (via Prisma). Infobip sends delivery reports via webhooks back to a RedwoodJS webhook handler, which updates the message status in the database.
Prerequisites:
- Node.js (18.x or higher recommended)
- Yarn (v1)
- RedwoodJS CLI installed (
yarn global add redwoodjs/cli) - An active Infobip account with API access
- An MMS-capable phone number provisioned in your Infobip account (e.g., a US 10DLC number)
- A local database setup (e.g., PostgreSQL, SQLite) configured for your RedwoodJS project (via
schema.prismaand.envDATABASE_URL) - Basic understanding of RedwoodJS, GraphQL, and TypeScript
1. Setting Up Your RedwoodJS MMS Project
Create a new RedwoodJS project and set up the necessary environment variables and dependencies.
-
Create RedwoodJS App: Open your terminal and run:
bashyarn create redwood-app ./redwood-infobip-mms cd redwood-infobip-mmsFollow the prompts (choose TypeScript).
-
Install Dependencies: Add
axiosto communicate with the Infobip API. Install it in the API side workspace:bashyarn workspace api add axios -
Configure Environment Variables: Create a
.envfile in the project root. Add the following variables, replacing the placeholder values with your actual Infobip credentials:dotenv# .env # Infobip API Credentials # Obtain from your Infobip account dashboard → API Keys Management INFOBIP_BASE_URL=YOUR_INFOBIP_API_BASE_URL # e.g., https://xyz.api.infobip.com INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY # Infobip Sender Number # An MMS-capable number from your Infobip account # Check Infobip documentation for required format (e.g., may need E.164 with '+', like +12223334444) INFOBIP_SENDER_NUMBER=YOUR_INFOBIP_MMS_NUMBER # e.g., 12223334444 # Webhook Security (Optional but Recommended) # A secret string you define, used to verify incoming webhooks INFOBIP_WEBHOOK_SECRET=YOUR_STRONG_RANDOM_SECRET # Application URL (for constructing webhook URLs) # Replace with your actual deployed URL in production APP_URL=http://localhost:8911INFOBIP_BASE_URL: Find this in your Infobip account documentation or dashboard. It's region-specificINFOBIP_API_KEY: Generate this in the Infobip dashboard under "API Keys Management". Treat this like a password – keep it secretINFOBIP_SENDER_NUMBER: The MMS-enabled number you'll send messages from, purchased in your Infobip account. Verify the required format (e.g., E.164 with or without '+') against Infobip's API documentationINFOBIP_WEBHOOK_SECRET: A secret you create. You'll use this later to add a basic layer of security to your webhook handlerAPP_URL: The base URL of your application, used for constructing thenotifyUrlfor webhooks
Important: Add
.envto your.gitignorefile to prevent accidentally committing secrets. RedwoodJS's default.gitignorealready includes it.
2. Implementing the Infobip MMS Service
Create a RedwoodJS service to encapsulate the logic for sending MMS messages via Infobip.
-
Generate Service:
bashyarn rw g service infobipMmsThis creates
api/src/services/infobipMms/infobipMms.tsand its test file. -
Implement
sendMmsFunction: Openapi/src/services/infobipMms/infobipMms.tsand add the following code:typescript// api/src/services/infobipMms/infobipMms.ts import axios from 'axios' import { logger } from 'src/lib/logger' interface SendMmsParams { to: string subject: string mediaUrl: string text?: string callbackData?: string // Data sent back with delivery report (JSON string recommended) } interface InfobipMmsResponse { bulkId?: string messages: { to: string status: { groupId: number groupName: string id: number name: string description: string } messageId?: string }[] } // Helper function to validate environment variables const getRequiredEnvVar = (varName: string): string => { const value = process.env[varName] if (!value) { logger.error(`Missing required environment variable: ${varName}`) throw new Error(`Configuration error: Missing ${varName}`) } return value } export const sendMms = async ({ to, subject, mediaUrl, text, callbackData, }: SendMmsParams): Promise<InfobipMmsResponse['messages'][0]> => { const baseUrl = getRequiredEnvVar('INFOBIP_BASE_URL') const apiKey = getRequiredEnvVar('INFOBIP_API_KEY') const senderNumber = getRequiredEnvVar('INFOBIP_SENDER_NUMBER') const appUrl = getRequiredEnvVar('APP_URL') // Get base URL for webhook // Construct the Infobip API endpoint URL // Verify the exact path in the official Infobip API documentation for MMS V1 const apiUrl = `${baseUrl}/mms/1/messaging` // Construct the payload // Infobip API expects mediaUrl and text within the 'content' object const payload = { messages: [ { from: senderNumber, to: [to], // API expects an array of recipients content: { subject: subject, mediaUrl: mediaUrl, text: text, // Optional text part }, // Include callbackData if provided, useful for correlating delivery reports ...(callbackData && { callbackData }), // Specify a URL for delivery reports (you'll create this webhook later) notifyUrl: `${appUrl}/.redwood/functions/infobipWebhook`, }, ], } try { logger.info({ payload }, `Sending MMS to ${to} via Infobip`) const response = await axios.post<InfobipMmsResponse>(apiUrl, payload, { headers: { Authorization: `App ${apiKey}`, 'Content-Type': 'application/json', Accept: 'application/json', }, timeout: 10000, // 10 second timeout }) logger.info( { infobipResponse: response.data }, `Infobip response received for MMS to ${to}` ) // Handle potential errors within the response structure const messageResult = response.data.messages?.[0] if (!messageResult) { throw new Error('Invalid response structure from Infobip') } // Example check for rejection status // **IMPORTANT**: Verify Infobip's status codes (groupId, groupName, id, name) // in their documentation. Relying solely on groupId=5 might be brittle // Consider checking status.groupName === 'REJECTED' or similar if (messageResult.status.groupId === 5) { // 5 often indicates REJECTED logger.error( { result: messageResult }, `MMS to ${to} rejected by Infobip` ) throw new Error( `Infobip rejected MMS: ${messageResult.status.description}` ) } // Return the details of the first (and only, in this case) message sent return messageResult } catch (error) { logger.error({ err: error, payload }, `Failed to send MMS via Infobip`) // Rethrow or handle specific errors (e.g., network errors, Infobip API errors) if (axios.isAxiosError(error)) { // Log detailed Axios error information logger.error( { message: error.message, code: error.code, status: error.response?.status, data: error.response?.data, }, 'Axios error during Infobip API call' ) // Extract more specific error message if available const errorMessage = error.response?.data?.requestError?.serviceException?.text || error.message throw new Error(`Infobip API request failed: ${errorMessage}`) } // Rethrow generalized error throw new Error(`Failed to send MMS: ${error.message || 'Unknown error'}`) } }Key Implementation Details:
- Define interfaces for the input parameters (
SendMmsParams) and the expected Infobip response (InfobipMmsResponse) - A helper
getRequiredEnvVarensures your critical environment variables are present - The
sendMmsfunction constructs the API URL and the JSON payload according to Infobip's expected format (usingmediaUrlandtextwithincontent) - Include the
Authorization: App YOUR_API_KEYheader required by Infobip - Use
axios.postto send the request - Include a
notifyUrlto tell Infobip where to send delivery report updates. Point it to a Redwood function (infobipWebhook) that you'll create later. Ensure this URL is publicly accessible in production (by settingAPP_URLcorrectly) - Implement basic response validation and error handling (including logging) using Redwood's logger and checking Infobip's status codes
- Specific handling for
groupId === 5(Rejected) is included as an example. Refer to Infobip documentation for a full list of status codes and implement more robust checks based ongroupNameor specificidvalues as needed
- Define interfaces for the input parameters (
3. Building the GraphQL API Layer
Expose the sendMms functionality through a secure GraphQL mutation.
-
Generate GraphQL SDL and Service: Create an SDL file specifically for MMS operations:
bashyarn rw g sdl MmsMessage --crudThis command creates the SDL file (
api/src/graphql/mmsMessages.sdl.ts) and a service file (api/src/services/mmsMessages/mmsMessages.ts) where you can place your mutation resolver. You'll adapt these generated files. -
Define the Mutation in SDL: Open
api/src/graphql/mmsMessages.sdl.tsand modify it as follows:typescript// api/src/graphql/mmsMessages.sdl.ts export const schema = gql` type MmsSendResponse { messageId: String status: String! description: String! to: String! } input SendMmsInput { to: String! subject: String! mediaUrl: String! text: String } type Mutation { """ Sends an MMS message via Infobip. Requires authentication. """ sendMms(input: SendMmsInput!): MmsSendResponse! @requireAuth } `- Define a custom response type
MmsSendResponseto return relevant information - Define a
SendMmsInputtype for the mutation arguments, ensuring required fields (to,subject,mediaUrl) - The
sendMmsmutation takes this input and returns the response @requireAuthdirective ensures only authenticated users can call this mutation (assuming you have RedwoodJS auth set up –yarn rw setup auth <provider>)
- Define a custom response type
-
Implement the Mutation Resolver: Open
api/src/services/mmsMessages/mmsMessages.ts(generated byg sdl) and implement the resolver for thesendMmsmutation. This integrates the Infobip service call with database logging:typescript// api/src/services/mmsMessages/mmsMessages.ts import { validate } from '@redwoodjs/api' // import type { RedwoodUser } from '@redwoodjs/auth' // Uncomment if using context.currentUser import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' import * as InfobipMmsService from 'src/services/infobipMms/infobipMms' interface SendMmsInputArgs { input: { to: string subject: string mediaUrl: string text?: string } } export const sendMms = async ({ input }: SendMmsInputArgs /*, { context }*/ ) => { // Uncomment context if using currentUser // Basic input validation (consider using zod for more complex validation) validate(input.to, 'Recipient Phone Number', { presence: true }) // Add more sophisticated phone number format validation if needed validate(input.subject, 'Subject', { presence: true, length: { max: 100 } }) validate(input.mediaUrl, 'Media URL', { presence: true, url: true }) const correlationId = crypto.randomUUID() logger.info({ correlationId, input }, 'sendMms mutation invoked') let mmsRecordId: string | null = null; // To store the DB record ID try { // Create initial DB record // This logs the attempt before calling the external service const initialRecord = await db.mmsMessage.create({ data: { recipient: input.to, subject: input.subject, mediaUrl: input.mediaUrl, text: input.text, status: 'PENDING', // Status before calling Infobip correlationId: correlationId, // Optional: Associate with the logged-in user // userId: context.currentUser?.id, }, select: { id: true } // Only select the ID you need }); mmsRecordId = initialRecord.id; logger.info({ mmsRecordId, correlationId }, 'Initial MMS record created in DB'); // Call the Infobip service function // Pass the database record ID and correlation ID in callbackData // This allows the webhook handler to easily find the record later const infobipResult = await InfobipMmsService.sendMms({ ...input, callbackData: JSON.stringify({ mmsRecordId, correlationId }), }) // Update DB record on successful submission to Infobip if (mmsRecordId) { await db.mmsMessage.update({ where: { id: mmsRecordId }, data: { status: 'SUBMITTED', // Status updated after successful submission infobipMessageId: infobipResult.messageId, infobipStatus: infobipResult.status.name, infobipStatusDescription: infobipResult.status.description, }, }) logger.info( { mmsRecordId, correlationId, infobipMessageId: infobipResult.messageId }, 'MMS record updated after successful Infobip submission' ) } logger.info( { correlationId, result: infobipResult }, 'Successfully submitted MMS to Infobip' ) // Return a structured response based on the SDL definition return { messageId: infobipResult.messageId, status: infobipResult.status.name, description: infobipResult.status.description, to: infobipResult.to, } } catch (error) { logger.error( { err: error, correlationId, input, mmsRecordId }, 'Error during sendMms mutation processing' ) // Update DB record on failure // If you managed to create a record before the error occurred, mark it as failed if (mmsRecordId) { try { await db.mmsMessage.update({ where: { id: mmsRecordId }, data: { status: 'FAILED_SUBMISSION', // Mark as failed during submission attempt infobipStatusDescription: error.message.substring(0, 255), // Store part of error message }, }) logger.info({ mmsRecordId, correlationId }, 'MMS record updated to FAILED_SUBMISSION'); } catch (dbError) { logger.error({ err: dbError, mmsRecordId, correlationId }, 'Failed to update MMS record status to FAILED_SUBMISSION') // Log this failure, but proceed to throw the original error } } // Re-throw the error to let GraphQL handle it // Consider mapping service errors to specific GraphQL errors for better client handling throw new Error(`Failed to send MMS: ${error.message}`) } } // Remove or comment out the default CRUD operations generated by `g sdl` // if you are not managing MmsMessage entities directly via this service /* export const mmsMessages = () => { ... } export const mmsMessage = ({ id }) => { ... } export const createMmsMessage = ({ input }) => { ... } export const updateMmsMessage = ({ id, input }) => { ... } export const deleteMmsMessage = ({ id }) => { ... } */- Import and call
InfobipMmsService.sendMms - Add basic input validation using Redwood's
validatefunction - Database Integration: Create an initial
MmsMessagerecord before calling Infobip. Include the resultingmmsRecordIdand acorrelationIdin thecallbackDatasent to Infobip (as a JSON string). Update the record after a successful Infobip submission or if an error occurs during the process - Logging includes the
correlationIdandmmsRecordIdfor better traceability - Structure the return value to match the
MmsSendResponsetype defined in the SDL - Error handling includes updating the DB record status and re-throwing errors for GraphQL
- The standard CRUD resolvers are commented out – you only need the
sendMmsmutation here
- Import and call
-
Testing the Mutation:
- Ensure you have RedwoodJS auth set up if you're using
@requireAuth. If not testing with auth, remove the directive temporarily - Run your development server:
yarn rw dev - Open the GraphQL Playground (usually
http://localhost:8911/graphql) - Execute the mutation (replace placeholders):
graphqlmutation SendMyMms { sendMms(input: { # Check Infobip docs for required format (e.g., may need '+', like +15551234567) to: "1_TARGET_PHONE_NUMBER" # e.g., 15551234567 subject: "Test MMS from Redwood!" mediaUrl: "URL_TO_A_PUBLIC_IMAGE" # e.g., https://www.infobip.com/wp-content/uploads/2021/09/06-infobip-logo-1.png text: "Hello from RedwoodJS and Infobip! Sent on [Current Date/Time]" # Use a generic or relevant date/text }) { messageId status description to } }- You should receive a response indicating success (e.g., status
PENDING_ENROUTE) or an error if something went wrong. Check your terminal logs and database for details
- Ensure you have RedwoodJS auth set up if you're using
4. Configuring Infobip Integration
Revisit the configuration and ensure secure handling of credentials.
-
API Key (
INFOBIP_API_KEY):- Purpose: Authenticates your application's requests to the Infobip API
- Format: A long alphanumeric string
- How to Obtain:
- Log in to your Infobip Portal/Dashboard
- Navigate to the API section (often named "API Keys Management" or similar)
- Generate a new API key. Give it a descriptive name (e.g., "RedwoodJS App Key")
- Copy the key immediately and store it securely in your
.envfile. You often cannot view the key again after initial generation - Do not commit
.envor the key to version control
-
Base URL (
INFOBIP_BASE_URL):- Purpose: Specifies the root URL for the Infobip API endpoints for your account's region
- Format: A URL like
https://<your_subdomain>.api.infobip.com - How to Obtain: This is usually provided in the Infobip documentation or visible in your account settings/API section. Ensure you use the correct URL for your account's datacenter/region
-
Sender Number (
INFOBIP_SENDER_NUMBER):- Purpose: The MMS-capable phone number messages will originate from
- Format: A phone number. Check Infobip's required format (e.g., E.164 with or without leading
+, like15551234567or+15551234567) - How to Obtain: Purchase or configure an MMS-capable number within the Infobip Portal (Channels → Numbers). Ensure it's provisioned correctly for MMS sending in the target region (e.g., US 10DLC registration)
-
Webhook Secret (
INFOBIP_WEBHOOK_SECRET):- Purpose: Used to verify that incoming webhook requests genuinely originate from Infobip (basic security)
- Format: A strong, random string you generate
- How to Obtain: You create this yourself. Use a password generator or a command like
openssl rand -base64 32. Store it in.env
5. Error Handling and Logging Best Practices
The service already includes basic try...catch blocks and logging. Enhance this approach:
-
Consistent Error Handling: The current approach logs errors and re-throws them from the resolver, letting GraphQL handle the response. For more granular client-side error handling, consider mapping specific errors (e.g., validation errors, Infobip rejections) to custom GraphQL errors
-
Logging: RedwoodJS uses Pino for logging. Add
logger.infoandlogger.errorcalls with context (correlationId,mmsRecordId). In production, configure log levels (LOG_LEVELenv var) and potentially forward logs to a dedicated service (Datadog, Logtail, etc.). UseLOG_FORMAT=jsonfor structured logging -
Retry Mechanisms: Network glitches or temporary Infobip issues might cause failures
- For API Calls: Sending MMS is often asynchronous (Infobip queues it). Retrying the initial submission can be risky (potential duplicates if the first request succeeded but the response failed). It's generally better to rely on Infobip's internal retries and monitor delivery reports via webhooks
- For Critical Sends: If initial submission failure is due to a potentially transient error (e.g., network timeout, 5xx error from Infobip), consider implementing a retry strategy with exponential backoff directly in the
infobipMmsservice, or preferably, by enqueueing the MMS request into a background job queue. Libraries likeasync-retrycan help implement this within the service
typescript// Example using async-retry (install with yarn workspace api add async-retry @types/async-retry) // Inside infobipMms service's sendMms function, wrap the axios call: import retry from 'async-retry'; // ... inside sendMms function ... try { const response = await retry( async (bail, attempt) => { logger.debug(`Attempting Infobip API call (attempt ${attempt})...`); try { const res = await axios.post<InfobipMmsResponse>(apiUrl, payload, { headers: { Authorization: `App ${apiKey}`, 'Content-Type': 'application/json', Accept: 'application/json', }, timeout: 10000, // 10 second timeout }); // Check for non-retryable Infobip response statuses (e.g., 4xx client errors) const messageResult = res.data.messages?.[0]; if (res.status >= 400 && res.status < 500) { logger.warn({ status: res.status, data: res.data }, 'Infobip returned non-retryable client error status.'); // bail throws the error, stopping retries bail(new Error(`Infobip non-retryable error: ${res.status}`)); return; // Added to satisfy TS, bail will throw first } // Check for explicit rejection (adapt based on Infobip docs) if (messageResult?.status?.groupName === 'REJECTED' || messageResult?.status?.groupId === 5) { logger.warn({ result: messageResult }, 'MMS Rejected by Infobip, not retrying.'); bail(new Error(`MMS Rejected: ${messageResult.status.description}`)); return; // Added to satisfy TS, bail will throw first } logger.debug('Infobip API call successful.'); return res; // Success, return the response } catch (error) { if (axios.isAxiosError(error)) { // Don't retry client errors (4xx) unless specific ones are known to be transient if (error.response && error.response.status >= 400 && error.response.status < 500) { logger.warn({ status: error.response.status, data: error.response.data }, 'Axios non-retryable client error status.'); bail(new Error(`Infobip non-retryable error: ${error.response.status}`)); return; // Added to satisfy TS, bail will throw first } logger.warn({ err: error }, 'Axios error occurred, will retry if possible.'); } else { logger.warn({ err: error }, 'Non-Axios error occurred, will retry if possible.'); } // Throw error to trigger retry for network issues, 5xx, etc. throw error; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay ms onRetry: (error, attempt) => { logger.warn({ attempt, err: error }, `Retrying Infobip MMS send (attempt ${attempt})`); }, } ); // Process successful response 'response.data' ... const messageResult = response.data.messages?.[0]; if (!messageResult) { // This case should ideally be caught by validation within the retry loop, // but handle defensively here too throw new Error('Invalid response structure from Infobip after retries'); } logger.info({ infobipResponse: response.data }, `Infobip response received after retries for MMS to ${to}`); return messageResult; // Return the successful result } catch (error) { logger.error({ err: error, payload }, `Failed to send MMS via Infobip after all retries`); // Handle final error after retries... // Ensure the error message passed up is informative throw new Error(`Failed to send MMS after retries: ${error.message}`); } -
Testing Error Scenarios:
- Temporarily use an invalid API key in
.envto test 401 errors - Use an invalid
mediaUrlformat to test Infobip's validation (likely a 400 error) - Disconnect your network temporarily to test network errors/timeouts/retries
- Send to an invalid or blocked phone number to test rejection statuses (
REJECTEDgroupName or specific error codes). Check Infobip docs for test numbers if available
- Temporarily use an invalid API key in
6. Database Schema and Message Tracking
Add a Prisma model to log sent messages and track their delivery status.
-
Define Prisma Model: Open
api/db/schema.prismaand add theMmsMessagemodel:prisma// api/db/schema.prisma datasource db { provider = "postgresql" // Or your chosen DB provider url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model MmsMessage { id String @id @default(cuid()) recipient String subject String mediaUrl String text String? status String @default("PENDING") // e.g., PENDING, SUBMITTED, FAILED_SUBMISSION, DELIVERED, FAILED_DELIVERY sentAt DateTime @default(now()) // Time of initial submission attempt // Infobip specific fields from initial response infobipMessageId String? @unique // The ID returned by Infobip upon submission infobipStatus String? // Status name from Infobip (initial response, e.g., PENDING_ENROUTE) infobipStatusDescription String? // Description from initial response // Delivery Report Status (from webhook) deliveryStatus String? // Final status from DLR (e.g., DELIVERED, FAILED, REJECTED, UNDELIVERABLE) deliveryTimestamp DateTime? // Time the final status was determined deliveryErrorCode Int? // Error code from DLR, if any deliveryErrorDescription String? // Error description from DLR, if any // Correlation & Tracking correlationId String? @unique // UUID for tracking across systems/logs callbackData String? // The callbackData string sent to Infobip // Optional: Link to User model if you have auth // userId String? // user User? @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@index([deliveryStatus]) @@index([sentAt]) @@index([recipient]) } // Example User model if linking messages: // model User { // id String @id @default(cuid()) // // ... other user fields // mmsMessages MmsMessage[] // Relation back to MMS messages // } -
Apply Migrations: Run the migration command to apply the schema changes to your database:
bashyarn rw prisma migrate dev --name add_mms_message_modelFollow the prompts to generate and apply the migration.
-
Update Service to Log Messages: The database logging logic is already incorporated into the
sendMmsmutation resolver inapi/src/services/mmsMessages/mmsMessages.tsas shown in Step 3. It handles creating a record before the API call and updating it based on the outcome (success or failure).
7. Security Best Practices for MMS API
Security is paramount, especially when dealing with external APIs and potential costs.
Authentication and Authorization
Already handled by @requireAuth on the mutation (ensure your RedwoodJS auth provider is correctly set up and configured). Add checks within the sendMms resolver if needed (e.g., requireAuth({ roles: 'admin' }), or custom logic like checking if context.currentUser.id is allowed to send MMS or send to the specific input.to number).
Input Validation Strategies
- Use Redwood's
validateor a library likezodfor robust validation ofto,subject,mediaUrl, andtext - Phone Numbers: Validating international phone numbers is complex. Use a dedicated library (e.g.,
google-libphonenumber) for strict E.164 validation if required. At minimum, check for plausible length and allowed characters (+and digits). Format should align with Infobip requirements - URLs: Ensure
mediaUrlis a valid, accessiblehttporhttpsURL. Prevent Server-Side Request Forgery (SSRF) by validating the URL protocol and potentially using a safe HTTP client or allow-listing domains if possible. Do not allowfile://or internal protocols - Content: Sanitize
textorsubjectif they might be displayed elsewhere in your application to prevent Cross-Site Scripting (XSS) attacks
Rate Limiting and Cost Control
Protect your API endpoint and webhook from abuse and accidental cost overruns:
- GraphQL Mutation: Implement rate limiting using RedwoodJS directives or middleware. Libraries like
graphql-rate-limit-directive(adapted for Redwood) or custom logic in the resolver can be used - Webhook Handler: Implement rate limiting and potentially IP allow-listing (if Infobip provides static IPs for webhooks) for the webhook function endpoint
Webhook Security Implementation
- Secret Verification: Implement logic in your webhook handler (
infobipWebhookfunction) to verify the request signature or a shared secret (INFOBIP_WEBHOOK_SECRET) passed in a header or the payload, as recommended by Infobip documentation. This prevents unauthorized requests to your webhook endpoint - HTTPS: Ensure your webhook endpoint uses HTTPS in production
Secrets Management
Never commit API keys (INFOBIP_API_KEY) or secrets (INFOBIP_WEBHOOK_SECRET) directly into your code or version control. Use .env locally and secure environment variable management in your deployment environment (e.g., Vercel Environment Variables, Netlify Build Environment Variables, AWS Secrets Manager, Doppler).
Frequently Asked Questions (FAQ)
What is MMS and how does it differ from SMS?
MMS (Multimedia Messaging Service) allows you to send messages containing multimedia content like images, videos, and audio files, while SMS (Short Message Service) is limited to text only (up to 160 characters). MMS messages can be much larger and support richer content for enhanced user engagement.
How does Infobip MMS API work with RedwoodJS?
Infobip provides REST API endpoints for sending MMS messages. Your RedwoodJS application creates a service layer that makes HTTP requests to Infobip's API, handles responses, and integrates with your database via Prisma. RedwoodJS's GraphQL layer exposes this functionality to your frontend application.
What phone number formats does Infobip accept for MMS?
Infobip typically accepts phone numbers in E.164 format, which includes the country code with or without a + prefix (e.g., +15551234567 or 15551234567). Check Infobip's specific documentation for your region, as requirements may vary. Always validate phone numbers before sending to avoid errors and costs.
How do I track MMS delivery status in RedwoodJS?
Use Infobip's webhook functionality by providing a notifyUrl in your API request. Infobip sends delivery reports (Delivery Receipts/DLRs) to this webhook endpoint. Your RedwoodJS function handler processes these webhooks and updates the message status in your Prisma database.
What is the cost of sending MMS through Infobip?
MMS pricing varies significantly by destination country, carrier, and message size. Infobip offers pay-as-you-go pricing and volume discounts. Contact Infobip sales or check their pricing page for specific rates. Always implement rate limiting and authorization to prevent unexpected costs.
How do I handle MMS failures and retries?
Implement retry logic with exponential backoff for transient errors (network timeouts, 5xx errors). Use libraries like async-retry in your service layer. For permanent failures (4xx errors, rejections), log the error in your database and notify administrators. Avoid retrying rejected messages to prevent duplicate sends.
Can I send MMS to international phone numbers?
Yes, but MMS availability and pricing vary significantly by country. Verify that your Infobip account has MMS enabled for the target country, ensure the destination carrier supports MMS, and check pricing before implementing international MMS sending. Some countries have limited or no MMS support.
How do I test MMS sending in development?
Use Infobip's test credentials and sandbox environment if available. Test with your own phone number first. Implement feature flags to control MMS sending in development vs. production. Mock the Infobip API responses in your unit tests to avoid actual API calls during testing.
What image formats and sizes work best for MMS?
MMS supports JPEG, PNG, and GIF formats. Keep images under 300-500 KB for best deliverability across carriers. Some carriers have stricter limits. Use publicly accessible HTTPS URLs for your mediaUrl. Optimize images before sending to reduce delivery time and improve success rates.
How do I implement MMS templates in RedwoodJS?
Create a separate service or database model for MMS templates that stores subject, text, and placeholder mediaUrl. Reference these templates in your sendMms mutation by template ID. Use template variables to personalize messages dynamically. This approach separates content management from delivery logic.
Next Steps and Advanced Features
Now that you have a working MMS implementation, consider these enhancements:
Production Deployment
- Environment Configuration: Set up production environment variables in your hosting platform (Vercel, Netlify, AWS)
- Database Migrations: Run Prisma migrations in your production database
- Webhook URL: Update
APP_URLto your production domain and ensure the webhook endpoint is publicly accessible via HTTPS - Monitoring: Implement logging and error tracking (Sentry, Datadog) to monitor MMS delivery success rates
Webhook Handler Implementation
Create the webhook function to process Infobip delivery reports:
// api/src/functions/infobipWebhook/infobipWebhook.ts
export const handler = async (event, _context) => {
// Verify webhook secret
// Parse delivery report
// Update database with delivery status
// Return 200 OK
}Queue-Based Processing
For high-volume MMS sending, implement a job queue:
- Use services like BullMQ with Redis
- Queue MMS requests instead of sending synchronously
- Implement retry logic in the queue worker
- Monitor queue health and processing rates
Message Templates and Personalization
- Create a
MessageTemplatePrisma model for reusable templates - Implement variable substitution for personalized content
- Build a template management UI in your RedwoodJS frontend
- Support multiple languages and localization
Analytics and Reporting
- Track MMS metrics (sent, delivered, failed, cost)
- Create dashboard queries using RedwoodJS services
- Implement GraphQL queries for message history
- Generate cost reports by user, campaign, or time period
Multi-Channel Messaging
Extend your implementation to support multiple channels:
- Add SMS fallback for failed MMS delivery
- Implement email as an alternative channel
- Create a unified messaging service abstraction
- Let users choose their preferred channel
This RedwoodJS MMS implementation provides a solid foundation for multimedia messaging in your application using Infobip's reliable API.
Frequently Asked Questions
How to send MMS messages with Infobip?
You can send MMS messages with Infobip by integrating their MMS API into your application. This typically involves making HTTP requests to Infobip's API endpoints with the recipient's phone number, message content, and any required media URLs. You will also need an active Infobip account and API key for authentication and an MMS-capable phone number provisioned with your Infobip account to send MMS messages from your RedwoodJS application.
What is RedwoodJS used for in MMS integration?
RedwoodJS is a full-stack JavaScript framework that simplifies building and structuring your application logic, including API interactions, database management, and integrating with services like Infobip's MMS API. Its structure streamlines the process of building the API and service layers within the backend of your RedwoodJS application.
Why does RedwoodJS use Prisma?
Prisma is the default Object-Relational Mapper (ORM) in RedwoodJS. It simplifies database interactions by allowing you to define your data models and access the database using a type-safe and intuitive API. It's used for database modeling, database access, and logging the MMS messages as well as delivery statuses.
When should I configure Infobip environment variables?
Configure your Infobip environment variables (API key, base URL, sender number, and webhook secret) before implementing the core MMS sending functionality. These variables are crucial for authenticating with Infobip, defining the API endpoint, specifying the sender number, and optionally adding security to your webhook.
Can I track MMS delivery status with Infobip?
Yes, you can track MMS delivery status using Infobip's webhook functionality. By setting up a webhook URL, Infobip will send delivery reports to your application, allowing you to update the message status in your database. This provides real-time feedback on whether a message has been successfully delivered, failed, or encountered other delivery issues.
How to set up Infobip MMS in RedwoodJS?
Set up Infobip MMS in RedwoodJS by creating a new RedwoodJS project, installing necessary dependencies (like Axios for HTTP requests), configuring Infobip environment variables, creating a dedicated RedwoodJS service to interact with the Infobip API, defining a GraphQL mutation to trigger sending messages, setting up a webhook handler for delivery reports and implementing Prisma models for data persistence. Finally, be sure to test all the integration components including the webhook and error handling capabilities.
What is the Infobip MMS API used for?
The Infobip MMS API is used for programmatically sending multimedia messages (MMS) containing text, images, and other media content. This enables sending richer notifications and visual communications directly from your application. Within RedwoodJS, it allows integration of Infobip features including multimedia messages and webhook capabilities to send and confirm delivery of MMS messages.
Why does Infobip need my base URL?
Infobip uses your base URL (`INFOBIP_BASE_URL`) to determine the correct regional API endpoint for your account. Infobip's API is distributed across different regions, so specifying the base URL ensures your application communicates with the right servers for optimal performance and correct functionality based on your region and location.
When should I validate input for sending MMS?
Input validation for sending MMS should be performed within your GraphQL mutation resolver before calling the Infobip service. Validating recipient phone numbers, message content, and URLs early prevents unnecessary API calls and ensures data integrity. RedwoodJS's validate function or third-party libraries can be used to verify phone numbers, the subject line, media URLs, and message content.
How to integrate Infobip API key in RedwoodJS?
Integrate your Infobip API key in RedwoodJS by storing it securely as an environment variable (`INFOBIP_API_KEY`) in a `.env` file. The RedwoodJS service then retrieves this key to include it in the `Authorization` header of every request made to the Infobip API. Remember to never expose or share your API key publicly, or check in to source control.
What is the role of Axios in Infobip integration?
Axios is used as a promise-based HTTP client to make API requests to Infobip. It simplifies the process of sending POST requests with the necessary headers and data payload to the Infobip MMS API endpoint. Axios is added to your RedwoodJS API side in order to communicate directly with Infobip's API services.
Why is a webhook secret important with Infobip?
A webhook secret adds a layer of security to your Infobip integration. It allows you to verify that incoming webhook requests are genuinely coming from Infobip and not from malicious sources. This secret should be a long random string that only you and Infobip know to ensure that webhook requests are from Infobip.
How to secure my Infobip MMS integration?
Secure your Infobip integration by using RedwoodJS's built-in authentication and authorization features, implementing robust input validation for phone numbers and content, using rate limiting to prevent abuse, and verifying webhook signatures with a shared secret. Be sure to validate and sanitize all input to prevent security issues.
When should I use error handling with Infobip?
Implement comprehensive error handling throughout your Infobip integration, including in the service that interacts with the API and the GraphQL resolver. Handling potential errors from the Infobip API, network issues, or database operations ensures your application remains resilient and provides useful feedback to users. Be sure to use try-catch blocks and informative error messages.