code examples

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

How to Send MMS with MessageBird API in Node.js & Fastify (2025 Guide)

Learn to send MMS messages using MessageBird API with Node.js and Fastify. Complete tutorial with code examples, webhook handling, error management, and deployment for US/Canada multimedia messaging.

Send MMS with MessageBird, Node.js & Fastify

Build a production-ready Node.js MMS messaging application using the high-performance Fastify framework and MessageBird API. This comprehensive tutorial shows you how to send Multimedia Messaging Service (MMS) messages with images, videos, and rich media content to recipients in the US and Canada. MessageBird is now branded as Bird, but the API endpoints and functionality remain unchanged.

This guide walks you through building a secure API endpoint that accepts recipient information and media URLs, then delivers MMS messages via MessageBird's REST API. You'll implement configuration management, authentication, error handling, webhook status tracking, testing, and deployment strategies.

What You'll Learn: MMS Integration Overview

Goal: Create a reliable and scalable Node.js service to send MMS messages programmatically using Fastify and MessageBird.

Problem Solved: Automate sending rich media messages (images, videos, PDFs) to users in the US and Canada, enabling richer communication for notifications, marketing campaigns, or user engagement compared to SMS-only messaging.

Technologies:

  • Node.js: JavaScript runtime for building server-side applications (v20 LTS or v22 LTS recommended for 2025).
  • Fastify: High-performance, low-overhead web framework for Node.js, known for its speed and excellent developer experience.
  • MessageBird MMS API: Communication platform enabling sending and receiving MMS messages.
  • axios: Promise-based HTTP client for making requests to the MessageBird API.
  • dotenv: Module to load environment variables from a .env file into process.env.
  • pino-pretty: Development tool to make Fastify's default JSON logs human-readable.
  • Docker (Optional): For containerizing the application for consistent deployment.

System Architecture:

mermaid
graph LR
    Client[Client Application / curl] -->|POST /send-mms (JSON Payload)| FastifyApp[Fastify Node.js App];
    FastifyApp -->|POST Request (API Key Auth)| MessageBird[MessageBird MMS API];
    MessageBird -->|MMS Delivery| Recipient[Recipient Phone];
    MessageBird -->|GET Status Update (Webhook)| FastifyApp;
    FastifyApp -->|Log Status| Logs[Application Logs / Monitoring];

    subgraph "Our Application"
        FastifyApp
        Logs
    end

    subgraph "External Services"
        MessageBird
        Recipient
    end

Prerequisites:

  • Node.js (v20 LTS or v22 LTS recommended for 2025; v18 reaches end-of-life in April 2025) and npm/yarn.
  • A MessageBird account with a Live API Access Key.
  • An MMS-enabled virtual mobile number (VMN) purchased through MessageBird, specifically for the US or Canada. This is your originator number.
  • Media files (images, etc.) hosted on publicly accessible URLs. MessageBird fetches media from these URLs.
  • Basic familiarity with Node.js, APIs, and the command line.

Expected Outcome: A Fastify application with a secure endpoint (/send-mms) that accepts MMS requests and relays them to the MessageBird API for delivery. The application also has an endpoint (/mms-status) to receive delivery status updates from MessageBird.


1. Node.js Project Setup for MessageBird MMS

Initialize your Node.js project and install the necessary dependencies for sending MMS with Fastify.

  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir fastify-messagebird-mms
    cd fastify-messagebird-mms
  2. Initialize Node.js Project: Create a package.json file.

    bash
    npm init -y

    (Use yarn init -y if you prefer Yarn)

  3. Install Dependencies: Install Fastify for the web server, axios to call the MessageBird API, and dotenv for managing environment variables.

    bash
    npm install fastify axios dotenv

    (Use yarn add fastify axios dotenv for Yarn)

  4. Install Development Dependencies: Install pino-pretty to format logs nicely during development.

    bash
    npm install --save-dev pino-pretty

    (Use yarn add --dev pino-pretty for Yarn)

  5. Create Project Structure: Create the basic files and directories.

    bash
    touch server.js .env .env.example .gitignore
    • server.js: Main application code.
    • .env: Stores sensitive credentials (API keys, etc.). Do not commit this file.
    • .env.example: A template showing required environment variables. Commit this file.
    • .gitignore: Specifies files/directories Git should ignore.
  6. Configure .gitignore: Add node_modules and .env to prevent committing them.

    text
    # .gitignore
    
    node_modules
    .env
    npm-debug.log
  7. Set up package.json Scripts: Add scripts for easily running the application.

    json
    {
      "scripts": {
        "start": "node server.js",
        "dev": "node server.js | pino-pretty"
      }
    }
    • npm start: Runs the server in production mode (standard logs).
    • npm run dev: Runs the server with human-readable logs via pino-pretty.

2. MessageBird API Configuration and Authentication

Manage credentials and settings securely using environment variables loaded via dotenv.

  1. Define Environment Variables (.env.example): List the required variables in .env.example.

    text
    # .env.example
    
    # MessageBird Configuration
    MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY
    MESSAGEBIRD_ORIGINATOR=YOUR_MESSAGEBIRD_MMS_ENABLED_NUMBER_E164
    
    # Application Configuration
    PORT=3000
    HOST=0.0.0.0
    FASTIFY_API_KEY=YOUR_SECURE_API_KEY_FOR_THIS_APP
    LOG_LEVEL=info
  2. Populate .env File: Create a .env file (or copy .env.example to .env) and fill in the actual values:

    • MESSAGEBIRD_API_KEY: Your Live Access Key from the MessageBird Dashboard (Developers > API access).
    • MESSAGEBIRD_ORIGINATOR: Your MMS-enabled US or Canadian number purchased from MessageBird, in E.164 format (e.g., +12015550123). This number must be explicitly enabled for MMS within your MessageBird settings.
    • PORT: The port your Fastify server listens on (default: 3000).
    • HOST: The host address (default: 0.0.0.0 to listen on all available network interfaces).
    • FASTIFY_API_KEY: A secret key you define. Clients calling your /send-mms endpoint need to provide this key for authentication. Generate a strong, random string.
    • LOG_LEVEL: Controls log verbosity (e.g., info, debug, warn, error).
  3. Load Environment Variables in server.js: At the very top of your server.js, load the variables.

    javascript
    // server.js
    require('dotenv').config();

3. Implementing the MMS Sending Route with Fastify

Now, build the core logic in server.js to send MMS messages using the MessageBird API.

  1. Basic Fastify Server Setup: Initialize Fastify and configure basic logging.

    javascript
    // server.js
    require('dotenv').config();
    const Fastify = require('fastify');
    const axios = require('axios');
    
    const fastify = Fastify({
      logger: {
        level: process.env.LOG_LEVEL || 'info',
        // Use pino-pretty only in development for readability
        ...(process.env.NODE_ENV !== 'production' && {
          transport: {
            target: 'pino-pretty',
            options: {
              translateTime: 'HH:MM:ss Z',
              ignore: 'pid,hostname',
            },
          },
        }),
      },
    });
    
    // Simple health check route
    fastify.get('/health', async (request, reply) => {
      return { status: 'ok' };
    });
    
    // --- MMS Sending Route will go here ---
    
    // --- Status Webhook Route will go here ---
    
    // Start the server
    const start = async () => {
      try {
        const port = parseInt(process.env.PORT || '3000', 10);
        const host = process.env.HOST || '0.0.0.0';
        await fastify.listen({ port, host });
        fastify.log.info(`Server listening on ${fastify.server.address().port}`);
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    start();
  2. Add Authentication Hook: We'll protect our /send-mms route using a simple API key check via a Fastify hook.

    javascript
    // server.js (Add this before defining routes that need protection)
    
    fastify.decorate('authenticate', async function (request, reply) {
      const apiKey = request.headers['x-api-key'];
      if (!apiKey || apiKey !== process.env.FASTIFY_API_KEY) {
        fastify.log.warn('Authentication failed: Invalid or missing API key');
        reply.code(401).send({ error: 'Unauthorized' });
        return Promise.reject(new Error('Unauthorized')); // Stop processing
      }
    });
  3. Define the /send-mms Route: This route will handle POST requests to send MMS messages.

    javascript
    // server.js (Add this after the authenticate hook)
    
    const sendMmsSchema = {
      body: {
        type: 'object',
        required: [], // Dynamically determined by oneOf/anyOf
        properties: {
          recipient: { type: 'string', description: 'Single recipient phone number in E.164 format.' },
          recipients: {
              type: 'array',
              items: { type: 'string' },
              description: 'Array of recipient phone numbers in E.164 format. Max 50 per request. Use either `recipient` or `recipients`, not both in the same call for clarity.'
          },
          subject: { type: 'string', maxLength: 256, description: 'MMS subject line.' },
          body: { type: 'string', maxLength: 2000, description: 'Text body of the MMS.' },
          mediaUrls: {
            type: 'array',
            items: { type: 'string', format: 'url' },
            maxItems: 10,
            description: 'Array of public URLs for media attachments (max 10).'
          },
          reference: { type: 'string', description: 'Optional client reference string.' },
          scheduledDatetime: { type: 'string', format: 'date-time', description: 'Optional schedule time (RFC3339 format: YYYY-MM-DDTHH:mm:ssZ).' }
        },
        // Ensure at least body or mediaUrls is provided
        anyOf: [
            { required: ['body'] },
            { required: ['mediaUrls'] }
        ],
        // Ensure only one recipient definition method is used and at least one is present
        oneOf: [
            { required: ['recipient'], properties: { recipient: { type: 'string'} }, not: { required: ['recipients']} },
            { required: ['recipients'], properties: { recipients: { type: 'array'} }, not: { required: ['recipient']} }
        ]
      },
      response: {
        200: {
          description: 'MMS message accepted by MessageBird for delivery.',
          type: 'object',
          properties: {
            id: { type: 'string' },
            href: { type: 'string' },
            direction: { type: 'string' },
            originator: { type: 'string' },
            subject: { type: ['string', 'null'] }, // Subject can be null if not sent
            body: { type: ['string', 'null'] }, // Body can be null if not sent
            mediaUrls: { type: 'array', items: { type: 'string' } },
            reference: { type: ['string', 'null'] },
            scheduledDatetime: { type: ['string', 'null'], format: 'date-time' },
            createdDatetime: { type: 'string', format: 'date-time' },
            recipients: { type: 'object' } // Detailed recipient status omitted for brevity
          }
        },
        400: {
            description: 'Bad Request - Invalid input data or validation failure.',
            type: 'object',
            properties: {
                error: { type: 'string', example: 'Bad Request' },
                message: { type: 'string' },
                details: { type: 'object' }
            }
        },
        401: {
            description: 'Unauthorized - Missing or invalid API key for this service.',
            type: 'object',
            properties: {
                error: { type: 'string', example: 'Unauthorized' }
            }
        },
        500: {
            description: 'Internal Server Error - Failure during processing or communicating with MessageBird.',
            type: 'object',
            properties: {
                error: { type: 'string', example: 'Internal Server Error'},
                message: { type: 'string'},
                details: { type: 'object'}
            }
        },
        503: {
            description: 'Service Unavailable - Could not reach MessageBird API.',
            type: 'object',
            properties: {
                error: { type: 'string', example: 'Service Unavailable'},
                message: { type: 'string'}
            }
        }
      }
    };
    
    fastify.post('/send-mms', { schema: sendMmsSchema, preHandler: [fastify.authenticate] }, async (request, reply) => {
      const { recipient, recipients, subject, body, mediaUrls, reference, scheduledDatetime } = request.body;
    
      // Determine the recipient list
      const recipientList = recipients || (recipient ? [recipient] : []); // Handle both cases
    
      if (recipientList.length === 0) {
           reply.code(400).send({ error: 'Bad Request', message: 'At least one recipient is required.' });
           return;
       }
      if (recipientList.length > 50) {
           reply.code(400).send({ error: 'Bad Request', message: 'Maximum of 50 recipients allowed per request.' });
           return;
      }
    
      const messageBirdPayload = {
        originator: process.env.MESSAGEBIRD_ORIGINATOR,
        recipients: recipientList,
        subject: subject,
        body: body,
        mediaUrls: mediaUrls,
        reference: reference,
        scheduledDatetime: scheduledDatetime
      };
    
      // Remove undefined/null fields cleanly before sending
      Object.keys(messageBirdPayload).forEach(key => (messageBirdPayload[key] === undefined || messageBirdPayload[key] === null) && delete messageBirdPayload[key]);
    
      try {
        fastify.log.info({ msg: 'Sending MMS request to MessageBird', payload: messageBirdPayload });
    
        const response = await axios.post('https://rest.messagebird.com/mms', messageBirdPayload, {
          headers: {
            'Authorization': `AccessKey ${process.env.MESSAGEBIRD_API_KEY}`,
            'Content-Type': 'application/json' // Explicitly set content type
          }
        });
    
        fastify.log.info({ msg: 'MessageBird API response received', messageId: response.data.id, status: response.status });
        reply.code(200).send(response.data); // Forward MessageBird's response
    
      } catch (error) {
        let statusCode = 500;
        let responseBody = {
            error: 'Internal Server Error',
            message: 'Failed to send MMS via MessageBird.',
            details: {}
        };
    
        if (error.response) {
          // Request made, server responded with non-2xx status
          fastify.log.error({ msg: 'MessageBird API Error', status: error.response.status, data: error.response.data });
          statusCode = error.response.status >= 500 ? 500 : 400; // Treat 4xx as 400, 5xx as 500
          responseBody.message = `MessageBird API error: ${error.response.data?.errors?.[0]?.description || 'Unknown error'}`;
          responseBody.details = error.response.data;
          responseBody.error = statusCode === 400 ? 'Bad Request' : 'Service Error';
    
        } else if (error.request) {
          // Request made, no response received
          fastify.log.error({ msg: 'MessageBird API No Response', error: error.message });
           responseBody.message = 'No response received from MessageBird API.';
           responseBody.error = 'Service Unavailable';
           statusCode = 503;
        } else {
          // Error setting up the request
          fastify.log.error({ msg: 'MMS Sending Error', error: error.message });
           responseBody.message = error.message;
        }
         reply.code(statusCode).send(responseBody);
      }
    });

    Explanation:

    • Schema: We define a schema using Fastify's built-in validation. This ensures incoming requests have the correct structure (recipient/recipients, body/mediaUrls), types, and constraints (lengths, max items) before our handler logic runs. It also specifies the expected success (200) and error (400, 401, 500, 503) response formats. anyOf ensures either body or mediaUrls is present. oneOf ensures only recipient or recipients is used and that at least one is provided.
    • preHandler: [fastify.authenticate]: This applies our authentication hook to this specific route, handling potential 401 errors.
    • Payload Construction: We build the JSON payload required by the MessageBird /mms endpoint, using the originator from environment variables and data from the incoming request. We clean up undefined or null optional fields. We add checks for recipient limits and presence (returning 400).
    • axios.post: We make the POST request to MessageBird's API endpoint.
      • The Authorization: AccessKey ... header is crucial for authenticating with MessageBird.
      • We explicitly set Content-Type: application/json.
    • Response Handling: If the request to MessageBird is successful (2xx), we log it and forward MessageBird's response back to our client with a 200 status.
    • Error Handling: If axios throws an error (network issue, non-2xx response from MessageBird), we catch it, log detailed information, determine an appropriate HTTP status code (400 for MessageBird client errors, 503 for timeouts, 500 for server errors), and send a structured error response back to our client.

4. How to Handle Media Attachments for MMS

MessageBird does not accept direct file uploads via the /mms endpoint. You must provide publicly accessible URLs.

  • Hosting: Upload your media (images, videos, etc.) to a service like AWS S3, Google Cloud Storage, Cloudinary, or even a simple public web server.
  • Public URLs: Ensure the URLs generated are publicly accessible without authentication. MessageBird's servers need to fetch the content from these URLs.
  • Latency: The media URLs should respond quickly (within 5 seconds according to MessageBird docs). Use a Content Delivery Network (CDN) for better performance if needed.
  • Size Limit: The total maximum file size of an MMS, including all media attachments, is 900 KB. The maximum file size after resizing for any individual attachment is 600 KB. MessageBird supports automatic resizing for images and GIFs up to 5 MB – if you submit a file that is too large, they will attempt to reduce its size to ensure delivery.
  • Count Limit: A maximum of 10 mediaUrls can be included in a single MMS request.
  • Supported Types: Refer to the MessageBird MMS API Documentation for the full list of supported Content-Type values. Common types include image/jpeg, image/png, image/gif, image/avif, video/mp4, audio/mpeg, audio/m4a, and application/pdf. Messages that fail due to unsupported attachments will record error code UNSUPPORTED_MEDIA_TYPE: 14004.

Example mediaUrls Array in Request Body:

json
{
  "recipient": "+14155550100",
  "subject": "Check this out!",
  "mediaUrls": [
    "https://your-cdn.com/images/logo.png",
    "https://your-public-bucket.s3.amazonaws.com/videos/promo.mp4"
  ]
}

5. Error Handling, Logging, and Retry Strategies

  • Error Handling: Our /send-mms route includes robust error handling for API calls to MessageBird. It catches different error types (API errors, network errors) and returns informative JSON responses with appropriate HTTP status codes (400, 401, 500, 503). Fastify's default error handler catches other unexpected errors.
  • Logging: Fastify's built-in logger (pino) is configured in server.js.
    • We log informational messages for successful requests/responses.
    • We log detailed error messages, including status codes and data from MessageBird API errors.
    • The LOG_LEVEL environment variable controls verbosity. Set to debug for more detailed logs during troubleshooting.
    • In development (npm run dev), logs are human-readable thanks to pino-pretty. In production (npm start), JSON logs are standard, which is better for log aggregation tools (like Datadog, Splunk, ELK stack).
  • Retries: Implementing retries can improve resilience against transient network issues when calling MessageBird.
    • Simple Retry (Conceptual): You could wrap the axios.post call in a simple loop or use a library like async-retry. Be cautious with retries for non-idempotent POST requests – ensure MessageBird handles duplicate requests gracefully if a retry occurs after the initial request succeeded but the response was lost. Using a unique reference field might help MessageBird deduplicate, but verify this behavior.
    • Recommendation: For production, start without automatic retries in the application layer unless specifically needed. Rely on monitoring to detect failures and potentially trigger manual or externally orchestrated retries if necessary, especially given the potential cost implications of sending duplicate messages. MessageBird itself might retry delivery on its end.

6. Security Best Practices for MessageBird MMS Integration

  • Input Validation: Done via Fastify's schema validation in the /send-mms route definition. This prevents malformed requests and basic injection attempts.
  • Authentication:
    • Your API: The /send-mms endpoint is protected by the x-api-key header check (fastify.authenticate hook). Ensure FASTIFY_API_KEY is strong and kept secret.
    • MessageBird API: The MESSAGEBIRD_API_KEY is sent securely via HTTPS in the Authorization header to MessageBird.
  • API Key Security: Never hardcode API keys in source code. Use environment variables (.env locally, secure configuration management in deployment). Rotate keys periodically. API access keys should be treated like passwords and never shared in publicly accessible areas like GitHub.
  • Rate Limiting: Protect your API from abuse by adding rate limiting.
    bash
    npm install @fastify/rate-limit
    javascript
    // server.js (Add near the top, after Fastify initialization)
    fastify.register(require('@fastify/rate-limit'), {
      max: 100, // Max requests per windowMs
      timeWindow: '1 minute'
    });
    This applies a global rate limit. You can configure it per-route if needed. Adjust max and timeWindow based on expected usage. Note: The @fastify/rate-limit plugin is compatible with both Fastify 4 and Fastify 5 (latest as of 2025).
  • HTTPS: Always run your application behind HTTPS in production (usually handled by a load balancer or reverse proxy like Nginx).
  • Dependency Security: Regularly update dependencies (npm audit fix or yarn audit) to patch known vulnerabilities.

7. Receiving MMS Delivery Status Updates via Webhooks

MessageBird can notify your application about the delivery status of sent MMS messages via webhooks.

  1. Configure Webhook URL in MessageBird:

    • Navigate to your MessageBird Dashboard.
    • Go to the "Numbers" section and select your MMS-enabled number.
    • Find the settings for incoming messages or webhooks. Look for a field like "Status reports URL" or similar specific to MMS/SMS if available. Note: The exact location might vary.
    • Enter the public URL where your Fastify application will be listening for status updates (e.g., https://your-app-domain.com/mms-status).
    • Ensure the method expected by MessageBird matches your implementation (GET for status reports as per docs).
  2. Create the Status Webhook Route in Fastify: MessageBird sends status updates as GET requests to your configured URL.

    javascript
    // server.js (Add this alongside other routes)
    
    fastify.get('/mms-status', async (request, reply) => {
      const { id, reference, recipient, status, statusDatetime } = request.query;
    
      // SECURITY: Verify webhook signature if configured
      // MessageBird uses MessageBird-Signature-JWT header with HMAC-SHA256 signature
      // Example verification (implement based on your signingKey):
      const signature = request.headers['messagebird-signature-jwt'];
      const signingKey = process.env.MESSAGEBIRD_SIGNING_KEY; // Optional: Set this in .env if you configure webhook signing
    
      if (signingKey && signature) {
        // TODO: Implement signature verification
        // Parse JWT, verify timestamp, compute HMAC-SHA256 of request URL + body hash
        // Compare computed signature with received signature
        // Reject request if verification fails
        fastify.log.debug('Webhook signature verification enabled (implement verification logic)');
      }
    
      // Log the received status update
      // In a real application, you would likely:
      // 1. Validate the request source (e.g., check IP, use signed requests if MessageBird supports it)
      // 2. Look up the message ID or reference in your database
      // 3. Update the message status in your database
      // 4. Trigger any necessary downstream actions based on the status (e.g., notify admins on failure)
    
      fastify.log.info({
        msg: 'Received MMS status update',
        messageId: id,
        reference: reference,
        recipient: recipient,
        status: status,
        statusTimestamp: statusDatetime
      });
    
      // IMPORTANT: Respond with 200 OK quickly!
      // MessageBird expects a 200 OK to acknowledge receipt.
      // Failure to respond promptly may cause MessageBird to retry the webhook.
      reply.code(200).send('OK');
    });

    Explanation:

    • The route listens on /mms-status for GET requests.
    • It extracts the status parameters from the query string (request.query).
    • Security Enhancement: Added webhook signature verification using MessageBird-Signature-JWT header. MessageBird signs HTTP requests using HMAC-SHA256 with your signing key to allow verification of authentication and integrity. If you create your webhook subscription using a signingKey, validate the authenticity by verifying the request signature.
    • It logs the received status. Crucially, add validation and database updates here in a real-world scenario.
    • It immediately sends a 200 OK response. This is vital for acknowledging receipt to MessageBird.
  3. Making Webhooks Accessible:

    • Local Development: Use a tool like ngrok (ngrok http 3000) to expose your local server (running on port 3000) to the internet with a public URL (e.g., https://<unique-id>.ngrok.io). Use this ngrok URL in the MessageBird dashboard for testing.
    • Production: Deploy your application to a hosting provider (see Deployment section) so it has a stable public domain name or IP address.

8. Testing Your MessageBird MMS Integration

Thorough testing ensures your MMS integration works correctly with MessageBird and Fastify.

  1. Unit Tests (Example using tap - Fastify's default):

    bash
    npm install --save-dev tap nock # nock for mocking HTTP

    Add test script to package.json:

    json
    {
      "scripts": {
        "start": "node server.js",
        "dev": "node server.js | pino-pretty",
        "test": "tap test/**/*.test.js"
      }
    }

    Create a test file test/routes/mms.test.js:

    javascript
    // test/routes/mms.test.js
    const { test } = require('tap');
    const Fastify = require('fastify');
    const nock = require('nock'); // For mocking HTTP requests
    const dotenv = require('dotenv');
    const axios = require('axios'); // Import axios to be used within the route handler
    
    // Load test environment variables if needed (e.g., from a .env.test file)
    // Ensure required env vars are set for tests
    process.env.FASTIFY_API_KEY = 'test-api-key';
    process.env.MESSAGEBIRD_API_KEY = 'mock_mb_key';
    process.env.MESSAGEBIRD_ORIGINATOR = '+10000000000';
    process.env.LOG_LEVEL = 'silent'; // Keep test output clean
    
    // Mock the authenticate decorator for testing routes in isolation
    function build(t) {
      const app = Fastify({ logger: { level: process.env.LOG_LEVEL } });
    
      app.decorate('authenticate', async function (request, reply) {
         // In tests, we can bypass actual auth check or mock it
         if (request.headers['x-api-key'] !== process.env.FASTIFY_API_KEY) {
              reply.code(401).send({ error: 'Unauthorized' });
              return Promise.reject(new Error('Unauthorized - Mock'));
         }
      });
    
      // Register your route (assuming server.js exports the app or setup function)
      // Simplified example: Directly defining the route logic here for clarity
      // Ideally, import and register the actual route handler from server.js
      const sendMmsSchema = { /* ... include schema if testing validation ... */ }; // Simplified for brevity
    
      app.post('/send-mms', {
         // schema: sendMmsSchema, // Add schema if needed
         preHandler: [app.authenticate]
      }, async (request, reply) => {
         // Replicate route logic for testing purposes
         const { recipient, recipients, subject, body, mediaUrls } = request.body;
         const recipientList = recipients || (recipient ? [recipient] : []);
    
         if (recipientList.length === 0) {
             return reply.code(400).send({ error: 'Bad Request', message: 'At least one recipient is required.' });
         }
         if (!body && !mediaUrls) {
             return reply.code(400).send({ error: 'Bad Request', message: 'Either body or mediaUrls is required.'});
         }
    
         const messageBirdPayload = {
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            recipients: recipientList,
            ...(subject && { subject: subject }),
            ...(body && { body: body }),
            ...(mediaUrls && { mediaUrls: mediaUrls }),
          };
    
         try {
            // Actual axios call will be intercepted by nock in tests
            const response = await axios.post('https://rest.messagebird.com/mms', messageBirdPayload, {
              headers: {
                'Authorization': `AccessKey ${process.env.MESSAGEBIRD_API_KEY}`,
                'Content-Type': 'application/json'
              }
            });
            reply.code(response.status).send(response.data);
          } catch (error) {
             if (error.response) {
                // Simulate the error handling logic from the actual route
                const statusCode = error.response.status >= 500 ? 500 : 400;
                const errorType = statusCode === 400 ? 'Bad Request' : 'Service Error';
                reply.code(statusCode).send({
                    error: errorType,
                    message: `MessageBird API error: ${error.response.data?.errors?.[0]?.description || 'Unknown error'}`,
                    details: error.response.data
                });
             } else if (error.request) {
                 reply.code(503).send({ error: 'Service Unavailable', message: 'No response received from MessageBird API.' });
             } else {
                reply.code(500).send({ error: 'Internal Server Error', message: error.message });
             }
          }
      });
    
      t.teardown(() => {
        app.close();
        nock.cleanAll(); // Clean up nock interceptors after tests
      });
      return app;
    }
    
    test('/send-mms route', async (t) => {
        t.test('should return 401 without API key', async (t) => {
            const app = build(t);
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                payload: { recipient: '+11112223344', body: 'Test' }
            });
            t.equal(response.statusCode, 401);
            t.match(response.json(), { error: 'Unauthorized' });
        });
    
         t.test('should return 401 with incorrect API key', async (t) => {
            const app = build(t);
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                 headers: { 'x-api-key': 'wrong-key' },
                payload: { recipient: '+11112223344', body: 'Test' }
            });
            t.equal(response.statusCode, 401);
            t.match(response.json(), { error: 'Unauthorized' });
        });
    
        t.test('should return 400 if recipient and recipients are missing', async (t) => {
            const app = build(t);
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                headers: { 'x-api-key': 'test-api-key' },
                payload: { body: 'Test' } // Missing recipient
            });
            t.equal(response.statusCode, 400);
            t.match(response.json(), { message: 'At least one recipient is required.' });
        });
    
        t.test('should return 400 if body and mediaUrls are missing', async (t) => {
            const app = build(t);
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                headers: { 'x-api-key': 'test-api-key' },
                payload: { recipient: '+11112223344' } // Missing body/mediaUrls
            });
            t.equal(response.statusCode, 400);
            t.match(response.json(), { message: 'Either body or mediaUrls is required.' });
        });
    
        t.test('should return 200 on successful mock MessageBird call', async (t) => {
            const app = build(t);
            const testPayload = {
                recipient: '+11112223344',
                body: 'Unit Test Message',
                mediaUrls: ['https://example.com/image.jpg']
            };
            const expectedMbPayload = {
                originator: process.env.MESSAGEBIRD_ORIGINATOR,
                recipients: [testPayload.recipient],
                body: testPayload.body,
                mediaUrls: testPayload.mediaUrls
            };
            const mockMbResponse = { id: 'mb-fake-id', status: 'sent', recipients: { totalSentCount: 1 } };
    
            // Use nock to intercept the outgoing HTTP request to MessageBird
            nock('https://rest.messagebird.com')
               .post('/mms', expectedMbPayload) // Match the payload axios would send
               .reply(200, mockMbResponse);
    
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                headers: { 'x-api-key': 'test-api-key' }, // Use the key expected by mock authenticate
                payload: testPayload
            });
    
            t.equal(response.statusCode, 200, 'Should return status code 200');
            t.match(response.json(), mockMbResponse, 'Response body should match mock MessageBird response');
            t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called'); // Ensure the mocked endpoint was called
        });
    
        t.test('should return 400 on MessageBird API client error (e.g., invalid recipient)', async (t) => {
            const app = build(t);
            const testPayload = { recipient: '+12223334455', body: 'Bad request test' };
            const mockMbErrorResponse = { errors: [{ code: 21, description: 'Recipient not valid', parameter: 'recipients' }] };
            const expectedMbPayload = {
                originator: process.env.MESSAGEBIRD_ORIGINATOR,
                recipients: [testPayload.recipient],
                body: testPayload.body
            };
    
            nock('https://rest.messagebird.com')
               .post('/mms', expectedMbPayload)
               .reply(422, mockMbErrorResponse); // MessageBird often uses 422 for validation
    
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                headers: { 'x-api-key': 'test-api-key' },
                payload: testPayload
            });
    
            t.equal(response.statusCode, 400, 'Should return status code 400'); // Our app maps 4xx to 400
            t.match(response.json(), {
                error: 'Bad Request',
                message: 'MessageBird API error: Recipient not valid',
                details: mockMbErrorResponse
            }, 'Response body should contain MessageBird error details');
            t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called');
        });
    
        t.test('should return 503 when MessageBird API does not respond', async (t) => {
            const app = build(t);
            const testPayload = { recipient: '+13334445566', body: 'Timeout test' };
    
            nock('https://rest.messagebird.com')
               .post('/mms')
               .delayConnection(100) // Simulate a delay
               .replyWithError({ code: 'ETIMEDOUT' }); // Simulate a timeout/network error
    
            const response = await app.inject({
                method: 'POST',
                url: '/send-mms',
                headers: { 'x-api-key': 'test-api-key' },
                payload: testPayload
            });
    
            t.equal(response.statusCode, 503, 'Should return status code 503');
            t.match(response.json(), { error: 'Service Unavailable' });
            t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called');
        });
    
        // Add more tests for other scenarios:
        // - Using 'recipients' array instead of 'recipient'
        // - Missing 'body' but providing 'mediaUrls'
        // - Providing 'subject', 'reference', 'scheduledDatetime'
        // - Exceeding recipient limit (should return 400 before calling MessageBird)
        // - MessageBird 5xx errors (should return 500)
    });
    • nock: Mocks HTTP requests to the MessageBird API, preventing actual calls during tests and allowing you to simulate success/error responses.
    • app.inject: Fastify's utility to simulate HTTP requests to your application without needing a running server.
    • Test Cases: Cover authentication failures, validation errors, successful calls (mocked), and various MessageBird API error scenarios (mocked).
  2. Integration Testing:

    • Run the application (npm run dev).
    • Use curl or a tool like Postman/Insomnia to send requests to http://localhost:3000/send-mms (or your configured port).
    • Requires .env: Ensure your .env file is populated with test credentials if possible, or be prepared to use live (but potentially costly) credentials carefully.
    • Test Payload:
      bash
      curl -X POST http://localhost:3000/send-mms \
        -H "Content-Type: application/json" \
        -H "x-api-key: YOUR_SECURE_API_KEY_FOR_THIS_APP" \
        -d '{
          "recipient": "+1xxxxxxxxxx",
          "body": "Hello from Fastify & MessageBird!",
          "mediaUrls": ["https://www.messagebird.com/assets/images/og/messagebird.png"]
        }'
    • Verify the response from your API.
    • Check your application logs for request/response details and errors.
    • Check the MessageBird dashboard logs to see if the request was received.
  3. Webhook Testing (using ngrok):

    • Start your server: npm run dev
    • Expose your local server: ngrok http 3000 (replace 3000 if using a different port). Note the public https://*.ngrok.io URL.
    • Configure the ngrok URL as the "Status reports URL" for your MessageBird number in their dashboard.
    • Send an MMS using your integration test method above.
    • Observe the logs in your running Fastify application. You should see log entries from the /mms-status route when MessageBird sends status updates.
    • Check the ngrok web interface (http://localhost:4040 by default) to inspect incoming webhook requests.

9. Deploying Your Fastify MMS Application to Production

Deploying the application makes it accessible publicly for sending MMS messages via MessageBird.

  1. Choose a Hosting Provider:

    • PaaS (Platform-as-a-Service): Heroku, Render, Google App Engine, AWS Elastic Beanstalk. Often simpler to manage.
    • IaaS (Infrastructure-as-a-Service): AWS EC2, Google Compute Engine, DigitalOcean Droplets. More control, more setup required.
    • Serverless: AWS Lambda + API Gateway, Google Cloud Functions. Scales automatically, pay-per-use (consider cold starts).
    • Containers: Docker + Kubernetes (EKS, GKE, AKS) or managed container services (AWS Fargate, Google Cloud Run).
  2. Prepare for Production:

    • Environment Variables: Configure environment variables securely on your hosting provider (do NOT commit .env). Use the provider's secrets management.
    • Build Step (if needed): If using TypeScript or a build process, ensure it runs before deployment.
    • NODE_ENV=production: Ensure the NODE_ENV environment variable is set to production. This disables development features (like pino-pretty) and enables optimizations in Fastify and other libraries.
    • Start Script: Ensure your package.json start script (node server.js) is correct.
    • HTTPS: Configure HTTPS (usually via a load balancer or reverse proxy provided by the host).
    • Logging: Configure log aggregation/monitoring (e.g., Datadog, Splunk, ELK, CloudWatch Logs, Google Cloud Logging). Production JSON logs are ideal for this.
    • Process Manager (if not using PaaS/Containers): Use pm2 or similar to manage the Node.js process (restarts on crash, clustering).
  3. Deployment Methods (Examples):

    • Heroku:
      • Install Heroku CLI.
      • heroku login
      • heroku create
      • Set environment variables: heroku config:set MESSAGEBIRD_API_KEY=... FASTIFY_API_KEY=... etc.
      • Ensure Procfile exists (usually inferred for Node.js): web: npm start
      • git push heroku main
    • Docker:
      • Create a Dockerfile:
        dockerfile
        # Dockerfile
        FROM node:18-alpine AS base
        WORKDIR /app
        
        # Install dependencies only when needed
        FROM base AS deps
        COPY package.json package-lock.json* ./
        RUN npm ci --omit=dev
        
        # Rebuild the source code only when needed
        FROM base AS builder
        COPY --from=deps /app/node_modules /app/node_modules
        COPY . .
        # Add build step here if necessary (e.g., RUN npm run build)
        
        # Production image, copy all the files and run next
        FROM base AS runner
        ENV NODE_ENV production
        # You can set other ENV variables here or via docker run/compose/k8s
        # ENV PORT=3000
        # ENV HOST=0.0.0.0
        # ENV MESSAGEBIRD_API_KEY=... (Better to pass these at runtime)
        
        COPY --from=builder /app/node_modules /app/node_modules
        COPY --from=builder /app /app
        
        EXPOSE ${PORT:-3000}
        CMD ["npm", "start"]
      • Build: docker build -t fastify-mms-app .
      • Run: docker run -p 3000:3000 -e MESSAGEBIRD_API_KEY=... -e FASTIFY_API_KEY=... fastify-mms-app (Pass secrets via -e or volume mounts).
      • Deploy the container image to your chosen container platform (Cloud Run, Fargate, Kubernetes, etc.).

Conclusion

You have successfully built a Node.js application using Fastify to send MMS messages via the MessageBird API. This service includes essential features like configuration management, authentication, robust error handling, logging, and webhook support for delivery status updates. Remember to prioritize security, thoroughly test your implementation, and choose a deployment strategy that fits your needs. This foundation allows you to integrate rich multimedia messaging into your applications effectively for users in the US and Canada.

For further MessageBird integration options, explore our guides on two-way MMS messaging and bulk broadcast campaigns.

Frequently Asked Questions

How to send MMS messages with Node.js?

Use a Node.js web framework like Fastify and the MessageBird MMS API. Create an endpoint that handles MMS requests, including recipient numbers, message body, media URLs, and other parameters. Then, use a library like Axios to send the request to MessageBird's API.

What is Fastify and why use it for MMS?

Fastify is a high-performance web framework for Node.js known for its speed and developer-friendly experience. Its efficiency makes it suitable for handling API requests, like those required for sending MMS messages with MessageBird.

Why does MessageBird mainly support MMS in US/Canada?

The article specifies that MessageBird's focus for MMS is the US and Canada. Ensure your recipients are in these regions. Check MessageBird's official documentation for the most up-to-date coverage information.

When should I use MMS instead of SMS?

Use MMS when you need to send rich media content like images or short videos along with text. MMS is ideal for notifications, marketing campaigns, and user engagement that benefits from visual elements, primarily in the US and Canada.

Can I send MMS messages internationally with MessageBird?

The article primarily describes MMS functionality with MessageBird for the US and Canada. It does not cover international MMS. Consult MessageBird's documentation for their current international MMS capabilities if you need to send messages outside the US and Canada.

How to handle MMS media attachments with MessageBird?

MessageBird requires public URLs for media files. Host your media on a publicly accessible service like AWS S3 or Cloudinary. Include an array of these URLs in the 'mediaUrls' field of your API request to MessageBird. Each file must be under 1MB, and you can include up to 10 URLs.

What is the 'originator' in MessageBird MMS?

The 'originator' is your MMS-enabled virtual mobile number (VMN) purchased from MessageBird, formatted in E.164 (e.g., +12015550123). This number must be specifically enabled for MMS in your MessageBird settings and is the number messages appear to come from.

How to secure my MessageBird API key?

Store your MessageBird API key and other sensitive credentials as environment variables. Load them using the `dotenv` package in development, but use secure configuration management in production deployments. Never commit these keys directly to your code repository.

What is the role of Axios in sending MMS?

Axios is a promise-based HTTP client that simplifies making API requests. It's used in this example application to send POST requests containing the MMS payload (recipient, message, media URLs, etc.) to the MessageBird MMS API endpoint.

How to receive MMS delivery status updates?

Configure a webhook URL in your MessageBird dashboard's number settings. MessageBird will send GET requests to this URL with status updates. In your application, create a route that handles these requests, logs the updates, and importantly, sends an immediate 200 OK response to acknowledge receipt.

How to test my MessageBird MMS integration?

Use a combination of unit tests (with mocking libraries like Nock) to test your application logic and integration tests (with tools like curl or Postman) to send real MMS messages to test numbers. Ensure your `.env` file is configured correctly and use test credentials if possible.

What is the purpose of the x-api-key header?

The `x-api-key` header provides an additional layer of security for *your* Fastify application's `/send-mms` endpoint. This prevents unauthorized access. The key is set in your `.env` file.

How to deploy my Fastify MessageBird MMS application?

Choose a hosting provider like Heroku, AWS, Google Cloud, etc. Set environment variables securely on the platform, ensure `NODE_ENV` is set to 'production', and configure logging and a process manager. Containerization with Docker and Kubernetes or managed container services is also recommended.

What is the size limit for MMS media attachments?

Each individual media file included in your MMS message must be 1MB (1024KB) or less, according to the MessageBird API specifications.

How many media attachments can I send in a single MMS?

You can include a maximum of 10 'mediaUrls' in a single MMS message request to the MessageBird API.