code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Article

Send MMS with Vonage in RedwoodJS: Complete Implementation Guide

Integrate Vonage MMS capabilities into your RedwoodJS app. Step-by-step guide for sending multimedia messages with images using the Vonage Messages API, GraphQL, and RedwoodJS services.

<!-- DEPTH: Introduction lacks business value context and real-world use case examples (Priority: Medium) -->

Integrate Vonage Multimedia Messaging Service (MMS) capabilities into your RedwoodJS application with this step-by-step walkthrough. Set up your environment, configure the Vonage Node.js SDK, create a RedwoodJS service and GraphQL mutation to send MMS messages, handle errors, and test your implementation.

Build a functional RedwoodJS backend capable of sending MMS messages containing images via the Vonage Messages API, complete with necessary configurations and error handling.

Project Overview and Goals

Goal: To build a feature within a RedwoodJS application that allows sending MMS messages (specifically, images with captions) to US phone numbers using the Vonage Messages API.

<!-- EXPAND: Could benefit from concrete business use cases and ROI considerations (Type: Enhancement) -->

Problem Solved: Provides a programmatic way to send rich media messages directly from your application backend, useful for notifications, alerts, or user engagement requiring images.

Technologies Used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. Provides structure (API services, GraphQL layer) and tooling. This guide requires RedwoodJS v7.0.0 or later (latest v8.x as of January 2025).
  • Node.js: The underlying runtime environment for the RedwoodJS API side. RedwoodJS v8.x requires Node.js v20.17.0 or later (Node.js >=20.x required).
  • Yarn: Package manager for RedwoodJS. Version v4.1.1 or later required for RedwoodJS v8.x.
  • Vonage Messages API: The third-party service used for sending MMS messages.
  • @vonage/messages SDK: The official Vonage Node.js client library for interacting with the Messages API (v1.20.3 as of January 2025).
  • GraphQL: Used by RedwoodJS for API communication between the frontend and backend.

Architecture:

text
+-----------------+      +---------------------+      +-----------------+      +-------------+
| RedwoodJS Frontend | ---> | RedwoodJS GraphQL API | ---> | MMS Service (API) | ---> | Vonage Messages API |
| (e.g., Web UI)  |      | (Mutation)          |      | (@vonage/messages)|      |             |
+-----------------+      +---------------------+      +-----------------+      +-------------+
       |                                                       ^
       |------------------- Env Variables (.env) ---------------|
                                (Credentials)
<!-- EXPAND: Architecture diagram could include webhook flow and data persistence layer (Type: Enhancement) -->

Prerequisites:

  • Basic understanding of JavaScript/TypeScript and Node.js.
  • Node.js v20.17.0 or later (Node.js >=20.x required for RedwoodJS) and Yarn v4.1.1 or later installed. Verify versions with node --version and yarn --version.
  • RedwoodJS CLI installed (yarn global add redwoodjs/cli).
  • A Vonage API account. (Sign up for free credit if needed).
  • A Vonage US phone number capable of sending SMS & MMS (10DLC, Toll-Free, or Short Code – check Vonage docs for specifics). All 10DLC campaigns are MMS-enabled as of January 2025. Purchase via the Vonage Dashboard (Numbers > Buy Numbers) or CLI.
  • Your Vonage API Key and API Secret (found on the Vonage API Dashboard).
  • A publicly accessible URL for the image you want to send. Vonage supports .jpg, .jpeg, .png, and .gif formats. Maximum file size: 600KB recommended (200KB optimal to avoid compression; up to 1MB for Short Codes only, though this may compromise delivery/quality). Source: Vonage MMS File Types and Vonage MMS File Size. For testing, https://placekitten.com/200/300 works.
<!-- GAP: Missing guidance on choosing between 10DLC, Toll-Free, and Short Code number types (Type: Substantive) -->

Outcome: A RedwoodJS application with a GraphQL mutation endpoint that accepts a recipient phone number, image URL, and caption, then uses the Vonage SDK to send an MMS message.

1. Set Up the RedwoodJS Project

First, create a new RedwoodJS project if you don't have one already.

bash
# Create a new RedwoodJS project (choose TypeScript)
yarn create redwood-app ./vonage-mms-app
cd vonage-mms-app

# Install the Vonage Messages SDK
yarn workspace api add @vonage/messages
<!-- DEPTH: Missing explanation of why workspace-specific installation matters in RedwoodJS (Priority: Low) -->

Environment Variables:

Securely store your Vonage credentials using environment variables. Create a .env file in the root of your project:

dotenv
# .env
VONAGE_API_KEY=YOUR_VONAGE_API_KEY
VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # Will get this in the next step
VONAGE_SENDER_NUMBER=YOUR_VONAGE_US_NUMBER # e.g., +15551234567
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to the api directory

<Callout type=""warn"" title=""Important""> Add .env and private.key (which we'll generate soon) to your .gitignore file to prevent accidentally committing credentials. </Callout>

text
# .gitignore (add these lines if not present)
.env
api/private.key

Project Structure:

  • api/: Contains your backend code (services, GraphQL schema, functions, libs).
  • api/src/lib/: Place for shared library code, like logger config.
  • api/src/services/: Where your business logic resides (we'll create mms/mms.ts).
  • api/src/graphql/: Defines your GraphQL schema (mms.sdl.ts).
  • web/: Contains your frontend code (optional for this guide, we'll test via GraphQL).
  • .env: Stores environment variables (ignored by Git).
  • private.key: Vonage private key file (will be placed in api/, ignored by Git).

2. Configure Vonage Application and Credentials

To use the Vonage Messages API for sending MMS via the SDK with JWT authentication (required), you need a Vonage Application.

<!-- DEPTH: Missing explanation of why JWT authentication is required vs basic auth (Priority: Medium) -->

Steps:

  1. Navigate to Vonage Dashboard: Go to your Vonage API Dashboard > Applications.
  2. Create New Application: Click ""Create a new application"".
  3. Name Application: Give it a descriptive name (e.g., ""RedwoodJS MMS Sender"").
  4. Enable Messages: Toggle the ""Messages"" capability ON.
  5. Configure Webhooks:
    • The SDK method we're using doesn't strictly require webhooks for sending, but the Application setup does. You must provide valid URLs. For testing, you can use a service like MockBin:
      • Go to MockBin.org.
      • Click ""Create Bin"".
      • Copy the generated endpoint URL (e.g., https://mockbin.org/bin/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
      • Paste this URL into both the ""Inbound URL"" and ""Status URL"" fields under the Messages capability.
    • <Callout type=""info"" title=""Production Note""> In a real application, these URLs would point to endpoints on your server to handle incoming messages and delivery status updates. </Callout>
<!-- GAP: Missing section on implementing production webhook handlers in RedwoodJS (Type: Substantive) -->
  1. Generate Public/Private Key Pair: Click the ""Generate public/private key pair"" link. This will:
    • Populate the ""Public key"" field in the dashboard.
    • Automatically download a private.key file to your computer. Move this private.key file into the api/ directory of your RedwoodJS project. (Ensure the VONAGE_PRIVATE_KEY_PATH in your .env file matches this location, e.g., ./private.key if it's directly in api/).
  2. Create Application: Click ""Create Application"".
  3. Get Application ID: You'll be redirected to the application's page. Copy the Application ID and paste it into your .env file for the VONAGE_APPLICATION_ID variable.
  4. Link Your Number: Scroll down to ""Link numbers to application"". Find your purchased Vonage US MMS-capable number and click ""Link"". This authorizes the application to send messages using that number.

Environment Variable Summary:

  • VONAGE_API_KEY: Your main account API key (Vonage Dashboard).
  • VONAGE_API_SECRET: Your main account API secret (Vonage Dashboard).
  • VONAGE_APPLICATION_ID: The ID of the Vonage Application you just created/configured.
  • VONAGE_SENDER_NUMBER: The linked, MMS-capable Vonage US number (include country code, e.g., +1...).
  • VONAGE_PRIVATE_KEY_PATH: The path to your private.key file, relative to the api directory (e.g., ./private.key).
<!-- EXPAND: Could add table comparing environment variable requirements across deployment platforms (Type: Enhancement) -->

3. Implement the MMS Sending Service

Now, let's create the RedwoodJS service that contains the core logic for sending the MMS.

bash
# Generate the mms service and GraphQL schema files
yarn rw g service mms
yarn rw g sdl mms # Generate the SDL file (we'll define the schema manually)

This creates:

  • api/src/services/mms/mms.ts
  • api/src/services/mms/mms.scenarios.ts
  • api/src/services/mms/mms.test.ts
  • api/src/graphql/mms.sdl.ts

Edit the Service File (api/src/services/mms/mms.ts):

Replace the contents with the following code:

<!-- DEPTH: Code lacks inline comments explaining RedwoodJS-specific patterns (Priority: Medium) -->
typescript
// api/src/services/mms/mms.ts
import type { MutationResolvers } from 'types/graphql'
import { Messages, MMSImage } from '@vonage/messages'
import { logger } from 'src/lib/logger'
import path from 'path' // Import path module

// Input type definition (matches the GraphQL input type)
interface SendMmsInput {
  to: string
  imageUrl: string
  caption?: string // Optional caption
}

// Initialize Vonage client ONCE outside the handler function for efficiency
// Ensure environment variables are loaded correctly
const vonageClient = new Messages({
  apiKey: process.env.VONAGE_API_KEY,
  apiSecret: process.env.VONAGE_API_SECRET,
  applicationId: process.env.VONAGE_APPLICATION_ID,
  // Resolve the private key path relative to the 'api' directory base
  privateKey: path.resolve(
    __dirname,
    '..', // Adjust relative path ('..') if build output moves this file deeper (e.g., 'dist/services/mms' might need '../..')
    process.env.VONAGE_PRIVATE_KEY_PATH || './private.key'
  ),
})

// Internal function containing the core sending logic
const performMmsSend = async ({ to, imageUrl, caption }: SendMmsInput) => {
  logger.info(`Attempting to send MMS to ${to}`)

  // Validate inputs (basic example)
  if (!to || !imageUrl || !process.env.VONAGE_SENDER_NUMBER) {
    logger.error('Missing required parameters for sending MMS.')
    throw new Error('Missing required parameters: to, imageUrl, or sender number.')
  }
  // Basic US number format check/normalization
  if (!to.startsWith('+1')) {
      logger.warn(`Recipient number ${to} might not be a valid US number. Prepending +1.`)
      to = to.startsWith('+') ? to : `+${to}` // Ensure it starts with +
      // WARNING: Use a proper validation library (e.g., google-libphonenumber) for production!
      if (!to.match(/^\+1\d{10}$/)) { // Basic check for +1 and 10 digits
          logger.error(`Invalid recipient US phone number format: ${to}`)
          throw new Error(`Invalid recipient US phone number format: ${to}`)
      }
  }


  try {
    const mmsPayload = new MMSImage({
      to: to,
      from: process.env.VONAGE_SENDER_NUMBER,
      image: {
        url: imageUrl,
        caption: caption || '', // Use provided caption or empty string
      },
      // You can add client_ref for tracking purposes
      // client_ref: `mms-send-${Date.now()}`
    })

    const response = await vonageClient.send(mmsPayload)

    logger.info({ response }, `MMS message submitted successfully to ${to}`)
    return {
      success: true,
      messageId: response.message_uuid,
      message: `MMS successfully submitted to ${to}. Message ID: ${response.message_uuid}`,
    }
  } catch (error) {
    logger.error({ error }, `Failed to send MMS to ${to}`)

    // Provide more context on the error if possible
    const errorMessage =
      error.response?.data?.title || error.message || 'Unknown error occurred'
    const errorDetail = error.response?.data?.detail || 'No details provided'

    // Rethrow a standard Error (simpler, caught by GraphQL layer) or return a structured error object (more GraphQL idiomatic, requires schema change):
    throw new Error(`Vonage API Error: ${errorMessage} - ${errorDetail}`)
    /*
    // Example of returning structured error (requires schema changes in SDL and response type)
    return {
      success: false,
      messageId: null,
      message: `Failed to send MMS: ${errorMessage} - ${errorDetail}`,
      // error: { code: 'VONAGE_ERROR', details: errorDetail } // Example structure
    }
    */
  }
}

// GraphQL Mutation Resolver
export const sendMms: MutationResolvers['sendMms'] = async ({ input }) => {
  // Delegate the actual sending logic to the internal function
  return performMmsSend(input)
}

Explanation:

  1. Imports: We import necessary types, the Vonage Messages and MMSImage classes, Redwood's logger, and Node.js path module.
  2. Input Interface: Defines the expected shape of data for sending an MMS.
  3. Vonage Client Initialization:
    • We create a single instance of the Messages client outside the resolver function. This is more efficient than creating it on every request.
    • Credentials (apiKey, apiSecret, applicationId) are read from process.env.
    • privateKey: We use path.resolve to correctly locate the private.key file relative to the current file's directory (__dirname) within the api structure. This is crucial for it to work correctly both in development and potentially in production builds where file paths might change. Adjust the relative path ('..') if your build process alters the directory structure significantly (e.g., outputting to a dist folder).
<!-- GAP: Missing example of handling private key from environment variable instead of file (Type: Substantive) -->
  1. performMmsSend Function:
    • Contains the core logic for input validation and sending the MMS.
    • Logs the attempt using Redwood's logger.
    • Includes basic validation for required fields (to, imageUrl, sender number) and a basic check/warning for the to number format (ensuring it starts with +1 and has the correct length). <Callout type=""warn"" title=""Warning""> Production applications should implement more robust validation, potentially using a dedicated library like google-libphonenumber to handle various formats and ensure validity. </Callout>
<!-- GAP: Missing code example of implementing google-libphonenumber validation (Type: Substantive) --> * Creates an `MMSImage` payload instance with `to`, `from` (from env vars), and `image` (URL and optional caption). * Calls `vonageClient.send(mmsPayload)`. * Uses a `try...catch` block for error handling (detailed below). * Logs success or failure. * Returns a structured object indicating success/failure and including the `message_uuid` from Vonage on success.

5. sendMms Resolver: This is the function RedwoodJS exposes via GraphQL. It simply takes the input object from the GraphQL mutation and passes it to the performMmsSend function.

<!-- EXPAND: Could add section on implementing URL validation and content moderation (Type: Enhancement) -->

4. Build the GraphQL API Layer

Now, define the GraphQL mutation that the frontend (or testing tools) will use to trigger the MMS sending service.

Edit the SDL File (api/src/graphql/mms.sdl.ts):

Replace the contents with the following schema definition:

graphql
# api/src/graphql/mms.sdl.ts
export const schema = gql`
  """"""Input type for the sendMms mutation""""""
  input SendMmsInput {
    """"""Recipient phone number (US format, e.g., +15551234567)""""""
    to: String!
    """"""Publicly accessible URL of the image to send (.jpg, .jpeg, .png)""""""
    imageUrl: String!
    """"""Optional caption for the image""""""
    caption: String
  }

  """"""Response type for the sendMms mutation""""""
  type SendMmsResponse {
    success: Boolean!
    """"""Vonage message UUID on success""""""
    messageId: String
    """"""User-friendly status message""""""
    message: String!
  }

  type Mutation {
    """"""Sends an MMS message via Vonage""""""
    sendMms(input: SendMmsInput!): SendMmsResponse! @skipAuth # Or use @requireAuth
  }
`

Explanation:

  1. SendMmsInput: Defines the input object structure for the mutation, matching the interface in our service. Fields are marked as required (!) where necessary. Descriptions clarify the expected format.
  2. SendMmsResponse: Defines the structure of the object returned by the mutation, indicating success, the Vonage message ID (if successful), and a status message.
  3. Mutation:
    • Defines the sendMms mutation.
    • It takes one argument: input of type SendMmsInput!.
    • It returns a SendMmsResponse!.
    • @skipAuth: This directive tells RedwoodJS not to enforce authentication for this mutation. <Callout type=""warn"" title=""Security Warning""> For production, you should almost always use @requireAuth (or implement role-based access control) to ensure only authorized users can send MMS messages, especially if it incurs costs. We use @skipAuth here for easier testing. </Callout>
<!-- GAP: Missing implementation example of role-based access control for MMS sending (Type: Substantive) -->

5. Implement Error Handling and Logging

Our service code already includes basic error handling and logging:

  • try...catch Block: The call to vonageClient.send() is wrapped in a try...catch.
  • Logging: Redwood's built-in logger (import { logger } from 'src/lib/logger') is used to log informational messages (logger.info) and errors (logger.error). Error logs include the caught error object for detailed debugging.
  • Error Propagation:
    • If vonageClient.send() throws an error, the catch block logs it.
    • It attempts to extract a user-friendly error message and detail from the Vonage API response (error.response.data).
    • It then throws a new Error which will be caught by Redwood's GraphQL layer and returned to the client as a GraphQL error. Alternatively, as noted in the code comment, you could modify the SendMmsResponse type to include an error field and return the structured error object instead of throwing (this is often considered more idiomatic for GraphQL but requires schema changes).
<!-- EXPAND: Could add comparison table of error handling approaches (throw vs return) (Type: Enhancement) -->

Common Vonage Errors & Troubleshooting:

  • 401 Unauthorized:
    • Cause: Incorrect API Key/Secret, incorrect Application ID, missing/incorrect Private Key, Private Key path incorrect in .env or code, Application ID not linked to the sending number, or the Vonage Application wasn't configured correctly for JWT authentication (using Application ID + Private Key is essential for Messages API).
    • Solution: Double-check all credentials in .env, ensure private.key is in the correct location (api/) and the path in mms.ts (path.resolve) is correct. Verify the number is linked to the application in the Vonage dashboard. Ensure the ""Messages"" capability is enabled for the application.
<!-- DEPTH: Error troubleshooting lacks step-by-step diagnostic procedure (Priority: Medium) -->
  • Invalid from Number: Ensure VONAGE_SENDER_NUMBER is correctly formatted (+1...) and is the number linked to your Vonage Application.
  • Invalid to Number: Ensure the recipient number is a valid US number (+1...). The service includes basic validation, but robust validation (e.g., using a library) is strongly recommended for production.
  • Invalid Image URL: Ensure imageUrl is publicly accessible and points directly to a .jpg, .jpeg, or .png file. Private URLs or URLs requiring logins will fail.
  • Feature Not Enabled: MMS sending might need specific provisioning on your Vonage account or number, especially for 10DLC compliance. Contact Vonage support if you suspect this.
  • Rate Limiting: Vonage may impose rate limits. Implement delays or use queues if sending bulk messages.
<!-- GAP: Missing specific rate limit numbers and how to handle 429 responses (Type: Substantive) -->

Log Analysis:

When troubleshooting, check the console output where you run yarn rw dev. Redwood's logger will print info and error messages, including details captured from Vonage API errors, which are invaluable for debugging.

Retry Mechanisms (Advanced):

For critical messages, you could implement a retry strategy (e.g., using a queue like BullMQ or integrating with RedwoodJS Background Jobs) with exponential backoff for transient network errors or Vonage API issues. This is beyond the scope of this basic guide.

<!-- GAP: Missing code example of implementing exponential backoff retry logic (Type: Substantive) -->

6. Database Schema and Data Layer (Optional)

For this specific guide (simply sending an MMS on demand), no database interaction is strictly required.

However, in a real-world application, you might want to:

  • Log MMS send attempts and statuses to a database table (MmsLog?).
  • Trigger MMS sends based on database events (e.g., new order confirmation).
  • Store user preferences related to notifications.
<!-- GAP: Missing Prisma schema example for MMS logging table (Type: Substantive) -->

If needed, you would use Prisma (Redwood's default ORM):

  1. Define your model in api/db/schema.prisma.
  2. Run yarn rw prisma migrate dev to create/update the database schema.
  3. Use the Prisma client (import { db } from 'src/lib/db') within your service to interact with the database.
<!-- EXPAND: Could add complete example of MMS audit logging implementation (Type: Enhancement) -->

7. Add Security Features

  • Credential Security:
    • NEVER commit API keys, secrets, or private keys to version control. Use .env and .gitignore.
    • Ensure private.key file permissions are restricted in production environments.
<!-- GAP: Missing specific file permission commands for production (Type: Substantive) -->
  • Authentication/Authorization:
    • Use @requireAuth or role-based directives on the sendMms mutation in mms.sdl.ts to restrict access. Define who can send messages and under what conditions.
  • Input Validation:
    • Sanitize and validate all inputs (to, imageUrl, caption).
    • Use libraries like zod or a dedicated phone number validation library (e.g., google-libphonenumber) for robust checks, especially for production.
    • Specifically validate phone number formats and potentially check against blocklists.
    • Validate imageUrl to ensure it's a valid URL format (though Vonage validates accessibility).
<!-- GAP: Missing zod schema validation example (Type: Substantive) -->
  • Rate Limiting:
    • Implement rate limiting on the GraphQL endpoint (e.g., using RedwoodJS directives or middleware if needed) to prevent abuse and manage costs. Vonage also has account-level rate limits.
<!-- GAP: Missing implementation example of GraphQL rate limiting in RedwoodJS (Type: Substantive) -->
  • Protect Against Common Vulnerabilities: While less critical for an outgoing-only API, always follow standard web security practices (OWASP Top 10).

8. Handle Special Cases

  • US Number Formatting: The Vonage Messages API generally expects E.164 format for US numbers (+1xxxxxxxxxx). The provided service code adds a basic check and normalization attempt, but production use requires more robust validation.
  • Image Requirements: Only publicly accessible .jpg, .jpeg, .png URLs are supported by Vonage MMS. Ensure the server hosting the image allows access from Vonage servers.
<!-- DEPTH: Missing guidance on hosting images securely for MMS (Priority: Medium) -->
  • Character Limits: Captions might have character limits imposed by carriers (though less strict than SMS). Keep them reasonably concise.
<!-- GAP: Missing specific caption character limit numbers (Type: Substantive) -->
  • Non-US Numbers: This setup using the standard Messages API with US numbers typically only supports sending to US numbers. Check Vonage documentation for international MMS capabilities if needed (often requires different setups/APIs).
  • Delivery Failures: MMS delivery isn't guaranteed. Implement status webhooks (as configured in the Vonage Application setup) to receive delivery receipts (DLRs) and handle failures (e.g., notify admins, retry via SMS using Vonage Dispatch API as suggested in their blog).

9. Optimize Performance

  • Client Initialization: The Vonage Messages client is initialized once outside the resolver, preventing unnecessary setup on each request.
  • Asynchronous Operations: The send() method is asynchronous (async/await), preventing the Node.js event loop from being blocked during the API call.
  • Payload Size: While image URLs are used, be mindful if generating images on the fly – keep processing efficient.
  • Bulk Sending: For high-volume sending, consider queuing mechanisms (like BullMQ with Redis) to manage load and retries, rather than direct synchronous calls within the request handler.
<!-- GAP: Missing BullMQ integration example for RedwoodJS (Type: Substantive) --> <!-- EXPAND: Could add performance benchmarks and optimization strategies (Type: Enhancement) -->

10. Monitor, Observe, and Analyze

  • Logging: Redwood's default logger provides basic observability. Configure log levels (api/src/lib/logger.ts) and potentially ship logs to a centralized logging service (e.g., Datadog, Logtail) in production.
  • Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) to capture and analyze runtime errors from the API side.
<!-- GAP: Missing Sentry integration example for RedwoodJS (Type: Substantive) -->
  • Vonage Dashboard: Monitor MMS usage, delivery rates, and costs directly within the Vonage API Dashboard.
  • Health Checks: Implement a basic health check endpoint in your RedwoodJS API (e.g., a simple GraphQL query or REST endpoint) that confirms the API is running.
<!-- EXPAND: Could add comprehensive monitoring dashboard implementation guide (Type: Enhancement) -->

11. Troubleshoot and Review Caveats

  • 401 Unauthorized: Check all credentials (API Key/Secret, App ID, Private Key path/content) and number linking. Ensure Messages capability is enabled and JWT auth is used (implicit with App ID/Private Key).
  • Invalid from/to: Verify number formats (E.164) and ensure the from number is linked and MMS-capable. Use robust validation for to numbers in production.
  • Invalid Image: URL must be public, direct link to jpg/jpeg/png.
  • Private Key Path: Double-check path.resolve in mms.ts and the location of private.key. Ensure the key file content is exactly as downloaded. Check build output paths if deploying.
  • Environment Variables: Ensure .env is loaded correctly (Redwood usually handles this) and variables are accessible in process.env. Restart the dev server after changing .env.
  • US-Only: Standard setup primarily targets sending to US numbers.
  • Delivery Not Guaranteed: Implement webhook handlers for delivery status updates for reliable tracking.
  • Costs: Sending MMS incurs costs. Monitor usage via the Vonage dashboard.
<!-- DEPTH: Troubleshooting section lacks flowchart or decision tree (Priority: Medium) -->

12. Deploy with CI/CD

  • Standard RedwoodJS Deployment: Follow RedwoodJS deployment guides for platforms like Vercel, Netlify, Render, or AWS Serverless.
  • Environment Variables: Crucially, you must configure the same environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_SENDER_NUMBER, VONAGE_PRIVATE_KEY_PATH) in your deployment environment's settings.
    • Private Key Handling: For VONAGE_PRIVATE_KEY_PATH, you have options:
      1. Store as Multi-line Env Var: Some providers let you paste the entire key content into an environment variable. You'd then need to adjust the mms.ts code to read the key content from process.env.VONAGE_PRIVATE_KEY_CONTENT instead of a file path.
      2. Secure File Storage: Some platforms allow uploading secure files. Upload private.key and set VONAGE_PRIVATE_KEY_PATH to its location in the deployment environment (e.g., /path/to/secure/files/private.key). This often requires build script adjustments to ensure the path is correct relative to the running code (consider the path.resolve logic).
      3. Base64 Encode: Encode the key content as Base64, store it in an env var, and decode it in your code before passing it to the Vonage client.
<!-- GAP: Missing complete code example for each private key deployment strategy (Type: Substantive) -->
  • CI/CD: Set up pipelines (e.g., using GitHub Actions) to automate testing and deployment. Ensure environment variables (except possibly the private key content itself) are configured as secrets in your CI/CD environment for tests.
<!-- GAP: Missing GitHub Actions workflow example for RedwoodJS + Vonage (Type: Substantive) -->

13. Verify and Test Your Implementation

Manual Verification:

  1. Start Dev Server: yarn rw dev

  2. Access GraphQL Playground: Open http://localhost:8911/graphql in your browser.

  3. Run Mutation: Execute the following mutation, replacing placeholders with your test recipient number (your own mobile is good for testing), a valid public image URL, and an optional caption:

    graphql
    mutation SendTestMms {
      sendMms(
        input: {
          to: ""+15559876543"" # Use a real US test number
          imageUrl: ""https://placekitten.com/200/300""
          caption: ""Test MMS from RedwoodJS!""
        }
      ) {
        success
        messageId
        message
      }
    }
  4. Check Phone: You should receive the MMS on the specified to number shortly.

  5. Check Logs: Observe the console output from yarn rw dev for success or error logs from the service.

<!-- EXPAND: Could add screenshot examples of expected GraphQL Playground results (Type: Enhancement) -->

Automated Testing (Unit/Integration):

RedwoodJS uses Jest for testing. Let's test the service logic.

Edit the Test File (api/src/services/mms/mms.test.ts):

Replace the contents with the following:

<!-- DEPTH: Test code lacks explanation of testing strategy and coverage goals (Priority: Medium) -->
typescript
// api/src/services/mms/mms.test.ts
import { Messages, MMSImage } from '@vonage/messages'
import { sendMms } from './mms' // Import the resolver function
import { logger } from 'src/lib/logger'

// Mock the Vonage Messages SDK
// We keep track of the send function mock to assert calls
const mockSend = jest.fn()
jest.mock('@vonage/messages', () => {
  // Mock the constructor and the send method
  return {
    Messages: jest.fn().mockImplementation(() => {
      return { send: mockSend } // Return an object with the mocked send method
    }),
    MMSImage: jest.fn().mockImplementation((args) => ({ ...args })), // Mock MMSImage constructor
  }
})

// Mock the logger to prevent actual logging during tests
jest.mock('src/lib/logger', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(), // Add warn if you use it
  },
}))

describe('mms service', () => {
  // Define sample input matching the GraphQL input type
  const validInput = {
    to: '+15551234567',
    imageUrl: 'https://valid.com/image.png',
    caption: 'Test Caption',
  }

  // Set required environment variables for the tests
  beforeAll(() => {
    process.env.VONAGE_SENDER_NUMBER = '+15550001111'
    // Mock other env vars if your client initialization depends on them being present,
    // even though the mock replaces the actual client.
    process.env.VONAGE_API_KEY = 'test-key'
    process.env.VONAGE_API_SECRET = 'test-secret'
    process.env.VONAGE_APPLICATION_ID = 'test-app-id'
    process.env.VONAGE_PRIVATE_KEY_PATH = './mock-private.key' // Path doesn't matter due to mock
  })

  // Clear mocks after each test
  afterEach(() => {
    jest.clearAllMocks()
  })

  it('should call Vonage SDK send with correct parameters on success', async () => {
    // Mock the Vonage API successful response
    const mockApiResponse = { message_uuid: 'mock-uuid-12345' }
    mockSend.mockResolvedValue(mockApiResponse)

    // Call the resolver function (which calls the internal logic)
    const result = await sendMms({ input: validInput })

    // Assertions
    expect(result.success).toBe(true)
    expect(result.messageId).toBe(mockApiResponse.message_uuid)
    expect(result.message).toContain('successfully submitted')

    // Check if Vonage client constructor was called (verifies mock setup)
    expect(Messages).toHaveBeenCalledTimes(1); // Verifies the mock constructor ran once during module load

    // Check if MMSImage constructor was called with correct args
    expect(MMSImage).toHaveBeenCalledWith({
      to: validInput.to,
      from: process.env.VONAGE_SENDER_NUMBER,
      image: {
        url: validInput.imageUrl,
        caption: validInput.caption,
      },
    })

    // Check if the send mock was called once with the MMSImage instance
    expect(mockSend).toHaveBeenCalledTimes(1)
    // Check the payload passed to send
    expect(mockSend).toHaveBeenCalledWith(
      expect.objectContaining({
        to: validInput.to,
        from: process.env.VONAGE_SENDER_NUMBER,
        image: { url: validInput.imageUrl, caption: validInput.caption },
      })
    )

    // Check logger calls (optional)
    expect(logger.info).toHaveBeenCalledWith(
      expect.stringContaining(`Attempting to send MMS to ${validInput.to}`)
    )
    expect(logger.info).toHaveBeenCalledWith(
      { response: mockApiResponse },
      expect.stringContaining(`MMS message submitted successfully`)
    )
    expect(logger.error).not.toHaveBeenCalled()
  })

  it('should throw an error and log if Vonage SDK fails', async () => {
    // Mock the Vonage API error response
    const mockError = new Error('Vonage API Error') as any
    mockError.response = {
      data: {
        title: 'Authentication Failed',
        detail: 'Your credentials are wrong',
      },
    }
    mockSend.mockRejectedValue(mockError)

    // Expect the resolver call to throw
    await expect(sendMms({ input: validInput })).rejects.toThrow(
      'Vonage API Error: Authentication Failed - Your credentials are wrong'
    )

    // Check logger calls
    expect(logger.error).toHaveBeenCalledTimes(1)
    expect(logger.error).toHaveBeenCalledWith(
      { error: mockError },
      expect.stringContaining(`Failed to send MMS to ${validInput.to}`)
    )
    expect(mockSend).toHaveBeenCalledTimes(1) // Ensure send was still attempted
  })

  it('should throw error for missing recipient number', async () => {
      const invalidInput = { ...validInput, to: '' } // Missing 'to'
      await expect(sendMms({ input: invalidInput })).rejects.toThrow(
          'Missing required parameters'
      )
      expect(mockSend).not.toHaveBeenCalled()
      expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Missing required parameters'))
  })

   it('should throw error for invalid US recipient number format (too short)', async () => {
      const invalidInput = { ...validInput, to: '+112345' } // Invalid format
      await expect(sendMms({ input: invalidInput })).rejects.toThrow(
          'Invalid recipient US phone number format'
      )
      expect(mockSend).not.toHaveBeenCalled()
      expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Invalid recipient US phone number format'))
   })

   it('should normalize and accept US number without +1 prefix', async () => {
      const inputWithoutPlus1 = { ...validInput, to: '15551234567' };
      const mockApiResponse = { message_uuid: 'mock-uuid-67890' };
      mockSend.mockResolvedValue(mockApiResponse);

      const result = await sendMms({ input: inputWithoutPlus1 });

      expect(result.success).toBe(true);
      expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('might not be a valid US number. Prepending +1.'));
      expect(mockSend).toHaveBeenCalledWith(
        expect.objectContaining({
          to: '+15551234567', // Expect normalized number
        })
      );
   });

  // Add more tests for edge cases: missing image URL, missing sender number
})
<!-- GAP: Missing integration tests for webhook handling (Type: Substantive) --> <!-- GAP: Missing tests for edge cases mentioned in comment (Type: Substantive) -->

14. Frequently Asked Questions (FAQ)

<!-- DEPTH: FAQ lacks troubleshooting decision tree and common error codes (Priority: Medium) -->

How do I send MMS messages with Vonage in RedwoodJS?

Send MMS messages with Vonage in RedwoodJS by: 1) Installing the @vonage/messages SDK in your API workspace (yarn workspace api add @vonage/messages), 2) Creating a Vonage Application with Messages capability enabled and generating a private key, 3) Creating a RedwoodJS service that initializes the Vonage Messages client with your Application ID and private key, 4) Creating a GraphQL mutation that accepts recipient phone number and image URL, and 5) Using vonageClient.send() with an MMSImage payload. The Vonage Messages client uses JWT authentication and requires a US-based 10DLC, Toll-Free, or Short Code number.

What file formats and sizes does Vonage MMS support?

Vonage MMS supports .jpg, .jpeg, .png, and .gif image formats. The maximum file size is 600KB recommended for reliable delivery. For optimal quality without compression, use 200KB or smaller. Short Codes can support up to 1MB, but this may compromise delivery quality and success rates. Images must be publicly accessible URLs – private URLs or those requiring authentication will fail. Source: Vonage MMS File Types and Vonage MMS File Size.

<!-- EXPAND: Could add comparison table of file format support across carriers (Type: Enhancement) -->

What Node.js and RedwoodJS versions are required for this implementation?

RedwoodJS v8.x (latest as of January 2025) requires Node.js v20.17.0 or later and Yarn v4.1.1 or later. Earlier RedwoodJS versions (v7.0.0+) work with Node.js >=20.x. Verify your versions with node --version and yarn --version. The @vonage/messages SDK (v1.20.3 as of January 2025) is compatible with these Node.js versions. RedwoodJS uses Node.js for its API side where the Vonage integration runs.

How do I fix "401 Unauthorized" errors with Vonage Messages API?

Fix "401 Unauthorized" errors by verifying: 1) Your VONAGE_APPLICATION_ID is correct (not your API Key), 2) The private.key file is in the correct location specified by VONAGE_PRIVATE_KEY_PATH, 3) The private key content matches exactly what was downloaded (no extra spaces or line breaks), 4) Your Vonage phone number is linked to the Application in the Vonage Dashboard, 5) The Messages capability is enabled on your Application, and 6) The path.resolve() logic in your service correctly locates the key file. JWT authentication requires the Application ID and private key – API Key/Secret alone won't work for the Messages API.

<!-- DEPTH: 401 troubleshooting lacks step-by-step verification commands (Priority: High) -->

Can I send MMS to international numbers with Vonage?

Vonage MMS primarily supports sending to US phone numbers only when using US-based 10DLC, Toll-Free, or Short Code numbers. International MMS capabilities are limited and may require different API configurations or number types. Check the Vonage Messages API documentation for current international MMS support. For international messaging, consider using WhatsApp, Viber, or other channels supported by the Vonage Messages API, which handle multimedia content globally.

<!-- GAP: Missing list of countries where MMS is supported (Type: Substantive) -->

How do I handle MMS delivery failures in RedwoodJS?

Handle MMS delivery failures by: 1) Configuring status webhooks in your Vonage Application to receive delivery receipts (DLRs), 2) Creating a RedwoodJS function or GraphQL subscription to process webhook callbacks, 3) Storing message status in your database using Prisma, 4) Implementing retry logic for transient failures using RedwoodJS Background Jobs (available in v8+), and 5) Monitoring delivery rates in the Vonage Dashboard. MMS delivery isn't guaranteed – carriers may reject messages due to content filters, recipient device limitations, or network issues. Always provide fallback options like SMS for critical notifications.

<!-- GAP: Missing code example of webhook handler implementation (Type: Critical) -->

What are the costs for sending MMS with Vonage?

Vonage charges per MMS message sent, with pricing varying by destination carrier and number type (10DLC, Toll-Free, Short Code). New accounts receive free credits for testing. Check your current balance and per-message rates in the Vonage API Dashboard. MMS is more expensive than SMS due to multimedia content. Monitor usage in production to prevent unexpected costs – implement rate limiting, user quotas, and cost alerts. Consider the Vonage Pricing page for detailed pricing by country and message type.

<!-- GAP: Missing specific price ranges and cost comparison table (Type: Substantive) -->

Do I need 10DLC registration to send MMS in the US?

Yes, 10DLC (10-Digit Long Code) registration is required to send MMS and SMS to US recipients using standard long code numbers. As of January 2025, all 10DLC campaigns are automatically MMS-enabled. Registration requires: 1) Brand registration with The Campaign Registry, 2) Campaign registration describing your use case (minimum 40 characters), 3) Sample messages, and 4) Reseller ID (required from January 2025). The process can take several days to weeks. Alternatively, use Toll-Free Numbers or Short Codes, which have different registration requirements. See Vonage 10DLC Documentation for complete registration guides.

<!-- GAP: Missing timeline estimates and approval rates for 10DLC registration (Type: Substantive) -->