code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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.prisma and .env DATABASE_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.

  1. Create RedwoodJS App: Open your terminal and run:

    bash
    yarn create redwood-app ./redwood-infobip-mms
    cd redwood-infobip-mms

    Follow the prompts (choose TypeScript).

  2. Install Dependencies: Add axios to communicate with the Infobip API. Install it in the API side workspace:

    bash
    yarn workspace api add axios
  3. Configure Environment Variables: Create a .env file 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:8911
    • INFOBIP_BASE_URL: Find this in your Infobip account documentation or dashboard. It's region-specific
    • INFOBIP_API_KEY: Generate this in the Infobip dashboard under "API Keys Management". Treat this like a password – keep it secret
    • INFOBIP_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 documentation
    • INFOBIP_WEBHOOK_SECRET: A secret you create. You'll use this later to add a basic layer of security to your webhook handler
    • APP_URL: The base URL of your application, used for constructing the notifyUrl for webhooks

    Important: Add .env to your .gitignore file to prevent accidentally committing secrets. RedwoodJS's default .gitignore already includes it.

2. Implementing the Infobip MMS Service

Create a RedwoodJS service to encapsulate the logic for sending MMS messages via Infobip.

  1. Generate Service:

    bash
    yarn rw g service infobipMms

    This creates api/src/services/infobipMms/infobipMms.ts and its test file.

  2. Implement sendMms Function: Open api/src/services/infobipMms/infobipMms.ts and 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 getRequiredEnvVar ensures your critical environment variables are present
    • The sendMms function constructs the API URL and the JSON payload according to Infobip's expected format (using mediaUrl and text within content)
    • Include the Authorization: App YOUR_API_KEY header required by Infobip
    • Use axios.post to send the request
    • Include a notifyUrl to 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 setting APP_URL correctly)
    • 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 on groupName or specific id values as needed

3. Building the GraphQL API Layer

Expose the sendMms functionality through a secure GraphQL mutation.

  1. Generate GraphQL SDL and Service: Create an SDL file specifically for MMS operations:

    bash
    yarn rw g sdl MmsMessage --crud

    This 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.

  2. Define the Mutation in SDL: Open api/src/graphql/mmsMessages.sdl.ts and 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 MmsSendResponse to return relevant information
    • Define a SendMmsInput type for the mutation arguments, ensuring required fields (to, subject, mediaUrl)
    • The sendMms mutation takes this input and returns the response
    • @requireAuth directive ensures only authenticated users can call this mutation (assuming you have RedwoodJS auth set up – yarn rw setup auth <provider>)
  3. Implement the Mutation Resolver: Open api/src/services/mmsMessages/mmsMessages.ts (generated by g sdl) and implement the resolver for the sendMms mutation. 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 validate function
    • Database Integration: Create an initial MmsMessage record before calling Infobip. Include the resulting mmsRecordId and a correlationId in the callbackData sent 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 correlationId and mmsRecordId for better traceability
    • Structure the return value to match the MmsSendResponse type 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 sendMms mutation here
  4. 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):
    graphql
    mutation 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

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:
      1. Log in to your Infobip Portal/Dashboard
      2. Navigate to the API section (often named "API Keys Management" or similar)
      3. Generate a new API key. Give it a descriptive name (e.g., "RedwoodJS App Key")
      4. Copy the key immediately and store it securely in your .env file. You often cannot view the key again after initial generation
      5. Do not commit .env or 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 +, like 15551234567 or +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.info and logger.error calls with context (correlationId, mmsRecordId). In production, configure log levels (LOG_LEVEL env var) and potentially forward logs to a dedicated service (Datadog, Logtail, etc.). Use LOG_FORMAT=json for 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 infobipMms service, or preferably, by enqueueing the MMS request into a background job queue. Libraries like async-retry can 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 .env to test 401 errors
    • Use an invalid mediaUrl format 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 (REJECTED groupName or specific error codes). Check Infobip docs for test numbers if available

6. Database Schema and Message Tracking

Add a Prisma model to log sent messages and track their delivery status.

  1. Define Prisma Model: Open api/db/schema.prisma and add the MmsMessage model:

    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
    // }
  2. Apply Migrations: Run the migration command to apply the schema changes to your database:

    bash
    yarn rw prisma migrate dev --name add_mms_message_model

    Follow the prompts to generate and apply the migration.

  3. Update Service to Log Messages: The database logging logic is already incorporated into the sendMms mutation resolver in api/src/services/mmsMessages/mmsMessages.ts as 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 validate or a library like zod for robust validation of to, subject, mediaUrl, and text
  • 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 mediaUrl is a valid, accessible http or https URL. 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 allow file:// or internal protocols
  • Content: Sanitize text or subject if 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 (infobipWebhook function) 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_URL to 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:

typescript
// 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 MessageTemplate Prisma 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.