code examples

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

Send MMS with Images Using Twilio Node.js and Express

Learn how to send MMS messages with images using Node.js, Express, and the Twilio Programmable Messaging API. Build a production-ready MMS sender from setup to deployment.

Dependencies

Learn how to send MMS messages with images using Node.js, Express, and the Twilio Programmable Messaging API. This comprehensive tutorial walks you through building a production-ready MMS sender from setup to deployment.

By the end, you'll have a functional Express API endpoint that sends multimedia messages (MMS) containing images to mobile phones using Twilio. Send rich media programmatically for notifications, alerts, marketing campaigns, or user engagement.

What You'll Build

Goal: Create a robust Node.js API endpoint to send MMS messages using Twilio.

Problem Solved: Enable applications to programmatically send images via MMS, overcoming the limitations of SMS-only communication.

Technologies:

TechnologyPurposeWhy Use It
Node.jsJavaScript runtime for server-side applicationsAsynchronous nature and large npm ecosystem
ExpressWeb application frameworkSimple API endpoint creation
Twilio Programmable MessagingSMS and MMS messaging APIIndustry-leading deliverability and MMS support
dotenvEnvironment variable managementSecure API credential handling

Prerequisites:

  • Node.js 14+ and npm installed
  • Twilio account with API credentials
  • Twilio phone number with SMS & MMS capability
  • Basic understanding of JavaScript, Node.js, REST APIs, and terminal commands
  • HTTP request tool (curl or Postman)

System Architecture:

The system flow works as follows:

  1. Client sends a POST request to your Express app
  2. Express app uses Twilio SDK (configured with Auth Token and Account SID) to communicate with Twilio API
  3. Twilio API sends the MMS to the recipient's mobile device
  4. Twilio sends delivery status updates to your webhook endpoint (optional)

Final Outcome: A running Express server with a /send-mms endpoint that accepts POST requests containing recipient number, image URL, and caption, then uses Twilio to send the MMS.

1. Set Up the Project

Initialize your Node.js project and install dependencies.

Create Project Directory:

Open your terminal and create a new directory for your project, then navigate into it.

bash
mkdir twilio-mms-sender
cd twilio-mms-sender

Initialize Node.js Project:

Create a package.json file to manage dependencies. The -y flag accepts default settings.

bash
npm init -y

Install Dependencies:

Install Express for the web server, the Twilio Node.js SDK to interact with the API, and dotenv to handle environment variables securely.

bash
npm install express twilio dotenv

What each package does:

  • express: Web framework for Node.js
  • twilio: Official Twilio library for Node.js
  • dotenv: Loads environment variables from .env into process.env

Create Project Files:

bash
touch index.js .env .gitignore

Configure .gitignore:

Prevent committing sensitive credentials or unnecessary files to version control. Add this to your .gitignore file:

Code
# Dependencies
node_modules

# Environment variables
.env

# Logs
*.log

# Operating system files
.DS_Store
Thumbs.db

Why .gitignore? This prevents accidental exposure of your API credentials and environment-specific configurations when using Git.

Project Structure:

Your project directory should now look like this:

twilio-mms-sender/ ├── .env ├── .gitignore ├── index.js ├── package.json ├── package-lock.json └── node_modules/

2. Obtain and Configure Twilio Credentials

To interact with the Twilio API, you need your Account SID and Auth Token. Configure these securely using environment variables.

Get Your Twilio Account SID and Auth Token

  1. Log in to your Twilio Console
  2. Find your Account SID and Auth Token on the console dashboard
  3. Click the eye icon to reveal your Auth Token
  4. Purpose: These credentials authenticate all requests to the Twilio API

Security Note: Never commit your Auth Token to version control or share it publicly. Treat it like a password.

Get a Twilio Phone Number

You need a Twilio phone number that supports MMS to send multimedia messages.

  1. Navigate to Phone NumbersManageBuy a number in the Twilio Console
  2. Select your country (US numbers have best MMS support)
  3. Check the MMS capability filter to show only MMS-capable numbers
  4. Choose a number and click Buy
  5. Note your purchased number in E.164 format (e.g., +14155552671)

How to verify MMS capability: When purchasing a number, ensure the MMS checkbox is checked in the capabilities section. US numbers typically support MMS, but always verify before purchase.

US A2P 10DLC Registration (for US SMS/MMS)

If you're sending messages to US recipients, you must register for A2P 10DLC (Application-to-Person 10-Digit Long Code) messaging.

  1. Navigate to MessagingRegulatory Compliance in the Twilio Console
  2. Complete the A2P 10DLC Registration form
  3. Submit your business information for verification
  4. Purpose: This ensures compliance with US carrier requirements and improves deliverability

Note: A2P 10DLC registration is required for production use with US phone numbers. For development and testing, you can use trial account limitations.

Set Environment Variables

Open the .env file and add your credentials. Replace placeholder values with your actual credentials.

Code
# Twilio API Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER=+14155552671

# Server Configuration
PORT=3000

Variable explanations:

  • TWILIO_ACCOUNT_SID: Your Twilio Account SID from the console
  • TWILIO_AUTH_TOKEN: Your Twilio Auth Token from the console
  • TWILIO_PHONE_NUMBER: Your MMS-capable Twilio phone number in E.164 format
  • PORT: The port your Express server will listen on

3. Implement Core Functionality and API Layer

Write the Node.js code to set up the Express server and the logic for sending MMS messages.

index.js

javascript
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const twilio = require('twilio');

const app = express();
app.use(express.json()); // Middleware to parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Middleware for URL-encoded bodies

// ---- Twilio Client Initialization ----
// Validate essential environment variables
const requiredEnv = [
    'TWILIO_ACCOUNT_SID',
    'TWILIO_AUTH_TOKEN',
    'TWILIO_PHONE_NUMBER'
];
requiredEnv.forEach(key => {
    if (!process.env[key]) {
        console.error(`Error: Missing required environment variable ${key}`);
        process.exit(1); // Exit if critical config is missing
    }
});

const client = twilio(
    process.env.TWILIO_ACCOUNT_SID,
    process.env.TWILIO_AUTH_TOKEN
);

// ---- API Endpoint for Sending MMS ----
app.post('/send-mms', async (req, res) => {
    console.log('Received request to /send-mms:', req.body);

    // Basic Input Validation
    const { to, imageUrl, caption } = req.body;
    const from = process.env.TWILIO_PHONE_NUMBER; // Sender number from .env

    if (!to || !imageUrl) {
        console.error('Validation Error: Missing `to` or `imageUrl` in request body');
        return res.status(400).json({ success: false, error: 'Missing required fields: `to` and `imageUrl`' });
    }

    // Validate 'to' number format (basic check for E.164 potential, allows optional +)
    if (!/^\+?[1-9]\d{1,14}$/.test(to)) {
        console.error(`Validation Error: Invalid 'to' number format: ${to}`);
        return res.status(400).json({ success: false, error: 'Invalid `to` number format. Use E.164 format (e.g., +14155552671).' });
    }

    // Ensure imageUrl is a valid URL (basic check)
    try {
        new URL(imageUrl);
    } catch (_) {
        console.error(`Validation Error: Invalid 'imageUrl': ${imageUrl}`);
        return res.status(400).json({ success: false, error: 'Invalid `imageUrl`. Must be a valid, publicly accessible URL.' });
    }

    // Construct the MMS payload for Twilio
    const mmsPayload = {
        body: caption || '', // Optional text caption
        from: from,
        to: to,
        mediaUrl: [imageUrl] // Array of media URLs (Twilio supports multiple)
    };

    console.log('Attempting to send MMS with payload:', JSON.stringify(mmsPayload, null, 2));

    // ---- Send the MMS using Twilio SDK ----
    try {
        const message = await client.messages.create(mmsPayload);
        console.log('Twilio API Success Response:', message);
        // Success response structure: { sid: '...', status: '...', ... }
        res.status(200).json({
            success: true,
            messageSid: message.sid,
            status: message.status
        });

    } catch (error) {
        console.error('Twilio API Error:', error);

        // Provide more specific feedback if possible
        let errorMessage = 'Failed to send MMS.';
        let statusCode = 500;
        let errorDetails = null;

        if (error.code) {
            // Twilio-specific error codes
            errorDetails = {
                code: error.code,
                message: error.message,
                moreInfo: error.moreInfo
            };

            // Handle common Twilio error codes
            if (error.code === 21408) {
                errorMessage = 'Permission denied: Check your Twilio account status or number capabilities.';
                statusCode = 403;
            } else if (error.code === 21614) {
                errorMessage = 'Invalid phone number format. Use E.164 format (e.g., +14155552671).';
                statusCode = 400;
            } else if (error.code === 21610) {
                errorMessage = 'Unsubscribed recipient: The recipient has opted out of messages from this number.';
                statusCode = 400;
            } else if (error.code === 63016) {
                errorMessage = 'Media file too large or invalid format. Keep images under 5MB.';
                statusCode = 400;
            } else {
                errorMessage = `Twilio Error ${error.code}: ${error.message}`;
                statusCode = error.status || 500;
            }
        } else if (error.status) {
            errorMessage = `Twilio API request failed with status: ${error.status}`;
            statusCode = error.status;
        } else {
            errorMessage = `Failed to send MMS: ${error.message}`;
        }

        res.status(statusCode).json({ success: false, error: errorMessage, details: errorDetails });
    }
});

// ---- Basic Health Check Endpoint ----
app.get('/health', (req, res) => {
    res.status(200).json({ status: 'UP' });
});

// ---- Start the Server ----
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
    console.log(`Twilio Number (Sender): ${process.env.TWILIO_PHONE_NUMBER}`);
    console.log(`Try POSTing to http://localhost:${PORT}/send-mms`);
});

Test Your Endpoint:

bash
# Start the server
node index.js

# In another terminal, send a test request
curl -X POST http://localhost:3000/send-mms \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552672",
    "imageUrl": "https://demo.twilio.com/owl.png",
    "caption": "Check out this image!"
  }'

Code Explanation:

SectionPurpose
require('dotenv').config()Loads variables from .env into process.env. Call this early.
express()Initializes the Express application.
app.use(express.json())Parses incoming JSON request bodies into req.body.
Environment Variable ValidationChecks essential Twilio credentials are set before initializing the client. Exits if any are missing.
twilio(accountSid, authToken)Creates a Twilio client instance using credentials from process.env.
app.post('/send-mms', ...)Defines the API endpoint as an async function to use await for the asynchronous client.messages.create call.
Input ValidationChecks for presence and basic format of to and imageUrl. Uses the from number configured in .env.
Payload ConstructionCreates the mmsPayload object with body, from, to, and mediaUrl array.
client.messages.create(mmsPayload)Makes the API call to Twilio to send the MMS.
Success HandlingLogs the response and sends a 200 OK JSON response with the message SID and status.
Error HandlingCatches errors during the API call, extracts user-friendly error messages from Twilio error codes, and sends appropriate HTTP status codes with JSON error messages.
/health EndpointA simple endpoint for monitoring services to check if the application is running.
app.listen(...)Starts the Express server on the specified PORT.

Security Note: Avoid logging sensitive data (like full API credentials) in production environments. The current implementation logs request bodies and payloads for debugging – remove or redact these logs before deploying to production.

4. Error Handling, Logging, and Retry Mechanisms

Error Handling:

The /send-mms route implements error handling using try...catch. It distinguishes between:

  • Validation errors (400)
  • Twilio API errors (using error codes from Twilio)
  • Server errors (500)

Specific error messages are extracted from Twilio error codes to help you diagnose issues quickly.

Common Twilio Error Codes:

Error CodeMeaningResolution
21408Permission deniedCheck account status, billing, or A2P 10DLC registration
21610Unsubscribed recipientRecipient has opted out; remove from list
21614Invalid phone numberUse E.164 format (+14155552671)
21211Invalid 'To' phone numberCheck recipient number format
63016Media errorReduce image size or check format (JPEG, PNG, GIF)

Logging:

Basic logging using console.log and console.error shows request reception, payload, API responses, and errors. For production, use a dedicated logging library like Winston or Pino for:

  • Structured logging (JSON format)
  • Different log levels (debug, info, warn, error)
  • Output to files or log management services
  • Performance optimization

Retry Mechanisms:

This basic example doesn't include automatic retries. For production robustness, especially for transient network issues, implement a retry strategy with exponential backoff.

When to retry:

  • Server errors (5xx)
  • Network timeouts
  • Rate limiting (429)

When NOT to retry:

  • Client errors (4xx, except 429)
  • Invalid credentials
  • Validation errors

Example with async-retry:

bash
npm install async-retry
javascript
const retry = require('async-retry');

// Inside the /send-mms route's main try block, after validation and payload construction:
try {
    // ... (input validation code remains here) ...
    // ... (mmsPayload construction remains here) ...
    console.log('Attempting to send MMS with payload:', JSON.stringify(mmsPayload, null, 2));

    // ---- Wrap only the Twilio call with retry logic ----
    const message = await retry(async bail => {
        // bail is a function to stop retrying for non-recoverable errors
        try {
            console.log('Calling client.messages.create...'); // Log each attempt
            const result = await client.messages.create(mmsPayload);
            console.log('Twilio API call successful within retry block.');
            return result; // Success, return result
        } catch (error) {
            console.error('Twilio API call failed within retry block:', error.code, error.message);

            // Don't retry on client errors (4xx) except 429 (Rate Limit)
            if (error.status && error.status >= 400 && error.status < 500 && error.status !== 429) {
                bail(new Error(`Non-retriable Twilio error: ${error.code}`));
                return;
            }
            // For other errors (5xx, network issues, 429), throw to trigger retry
            throw error;
        }
    }, {
        retries: 3, // Number of retries
        factor: 2, // Exponential backoff factor
        minTimeout: 1000, // Initial delay ms (1 second)
        maxTimeout: 10000, // Maximum delay between retries (10 seconds)
        onRetry: (error, attempt) => {
            console.warn(`Twilio API call failed (attempt ${attempt}), retrying... Error: ${error.message}`);
        }
    });

    console.log('Twilio API Success Response (after potential retries):', message);
    res.status(200).json({
        success: true,
        messageSid: message.sid,
        status: message.status
    });

} catch (error) {
    // Handle final errors after all retries
    console.error('Error sending MMS (final):', error);
    // ... error response handling
}

Retry Best Practices:

  • Set a maximum total time limit (e.g., 30 seconds) to prevent indefinite hanging
  • Implement jitter to prevent thundering herd problems
  • Log each retry attempt for debugging
  • Monitor retry rates to detect upstream issues

5. Database Integration (Optional)

This guide focuses on the API endpoint for sending MMS. A production application would likely require a database to:

  • Log sent messages (SID, recipient, status, timestamp)
  • Store user data
  • Manage templates or image assets
  • Track delivery statuses received via webhooks

Example Database Schema:

sql
-- PostgreSQL example
CREATE TABLE mms_messages (
    id SERIAL PRIMARY KEY,
    message_sid VARCHAR(255) UNIQUE NOT NULL,
    recipient_number VARCHAR(20) NOT NULL,
    sender_number VARCHAR(20) NOT NULL,
    image_url TEXT NOT NULL,
    caption TEXT,
    status VARCHAR(50) DEFAULT 'queued',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE delivery_receipts (
    id SERIAL PRIMARY KEY,
    message_sid VARCHAR(255) REFERENCES mms_messages(message_sid),
    status VARCHAR(50) NOT NULL,
    timestamp TIMESTAMP NOT NULL,
    error_code VARCHAR(50),
    error_message TEXT
);

CREATE INDEX idx_message_sid ON mms_messages(message_sid);
CREATE INDEX idx_recipient ON mms_messages(recipient_number);
CREATE INDEX idx_status ON mms_messages(status);

Recommended Databases:

DatabaseBest ForConsiderations
PostgreSQLStructured data, ACID complianceMature, feature-rich, good for complex queries
MongoDBFlexible schema, document storageNoSQL, good for rapid development
MySQLStructured data, wide hosting supportPopular, well-documented
RedisCaching, temporary dataFast, in-memory, good for rate limiting

ORMs to Consider:

  • Prisma: Type-safe, modern, excellent developer experience
  • Sequelize: Mature, feature-rich, supports multiple databases
  • Mongoose: MongoDB-specific, schema validation
  • TypeORM: TypeScript-first, decorator-based

6. Add Security Features

Secrets Management:

Handle credentials via .env and .gitignore. Never commit .env. Use environment variables provided by your deployment platform in production (see Section 11).

Best practices:

  • Rotate API credentials regularly
  • Use separate credentials for development, staging, and production
  • Store credentials in secure secret management services (AWS Secrets Manager, HashiCorp Vault)
  • Implement key rotation policies
  • Monitor credential usage for anomalies

Input Validation:

Basic validation is implemented for to and imageUrl. Enhance this as needed:

bash
npm install joi
javascript
const Joi = require('joi');

// Define validation schema
const mmsSchema = Joi.object({
    to: Joi.string()
        .pattern(/^\+?[1-9]\d{1,14}$/)
        .required()
        .messages({
            'string.pattern.base': 'Invalid phone number format. Use E.164 format (e.g., +14155552671)',
            'any.required': 'Recipient number is required'
        }),
    imageUrl: Joi.string()
        .uri()
        .required()
        .messages({
            'string.uri': 'Invalid image URL',
            'any.required': 'Image URL is required'
        }),
    caption: Joi.string()
        .max(1000)
        .allow('')
        .optional()
});

// In your route handler
const { error, value } = mmsSchema.validate(req.body);
if (error) {
    return res.status(400).json({
        success: false,
        error: error.details[0].message
    });
}

Rate Limiting:

Protect your API from abuse:

bash
npm install express-rate-limit
javascript
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
    legacyHeaders: false, // Disable the `X-RateLimit-*` headers
    message: 'Too many requests from this IP, please try again after 15 minutes',
    handler: (req, res) => {
        res.status(429).json({
            success: false,
            error: 'Too many requests, please try again later'
        });
    }
});

// Apply rate limiting to specific endpoints
app.use('/send-mms', limiter);

For multi-server deployments, use a shared store:

bash
npm install rate-limit-redis redis
javascript
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const redisClient = redis.createClient({
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT
});

const limiter = rateLimit({
    store: new RedisStore({
        client: redisClient,
        prefix: 'rate-limit:'
    }),
    windowMs: 15 * 60 * 1000,
    max: 100
});

HTTPS:

Always use HTTPS in production. Deployment platforms often handle SSL termination, or configure it in your load balancer or directly in Node.js.

Authentication and Authorization:

Protect your API endpoint with authentication:

javascript
// Simple API key authentication
const authenticateApiKey = (req, res, next) => {
    const apiKey = req.headers['x-api-key'];

    if (!apiKey || apiKey !== process.env.API_KEY) {
        return res.status(401).json({
            success: false,
            error: 'Unauthorized: Invalid or missing API key'
        });
    }

    next();
};

// Apply to protected routes
app.post('/send-mms', authenticateApiKey, async (req, res) => {
    // ... route handler code
});

CORS Configuration:

If your API will be called from a frontend application:

bash
npm install cors
javascript
const cors = require('cors');

// Allow specific origins
app.use(cors({
    origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key']
}));

Common Vulnerabilities:

Be mindful of OWASP Top 10:

  • Injection: Validate and sanitize all inputs
  • Broken Authentication: Use strong authentication mechanisms
  • Sensitive Data Exposure: Never log credentials or sensitive data
  • XML External Entities (XXE): Not applicable for this JSON API
  • Broken Access Control: Implement proper authorization
  • Security Misconfiguration: Follow security best practices
  • Cross-Site Scripting (XSS): Less relevant for backend APIs
  • Insecure Deserialization: Validate data before processing
  • Using Components with Known Vulnerabilities: Keep dependencies updated
  • Insufficient Logging & Monitoring: Implement comprehensive logging

7. Handle Special Cases

MMS Support by Region:

Twilio MMS sending is primarily supported in certain regions. Coverage varies by country and carrier.

Currently supported regions:

  • United States (full MMS support)
  • Canada (full MMS support)
  • Select other countries (check Twilio MMS documentation for current list)

International considerations: When sending MMS internationally, messages may fall back to SMS with a link to the media, or fail entirely. Always consult the official Twilio documentation for the latest supported regions.

Image URL Accessibility:

The imageUrl must be publicly accessible over the internet for Twilio to fetch and attach it. URLs behind firewalls or requiring authentication will fail.

Recommended image hosting:

ServiceProsCons
AWS S3Reliable, scalable, global CDNRequires AWS account, setup complexity
CloudinaryImage optimization, transformationsCosts can increase with usage
ImgurSimple, free tier availableLess control, may have restrictions
Your own CDNFull controlRequires infrastructure management

Supported Image Types and Sizes:

Twilio typically supports:

FormatMax File SizeNotes
JPEG (.jpg, .jpeg)5 MBMost widely supported
PNG (.png)5 MBSupports transparency
GIF (.gif)5 MBAnimated GIFs supported, but may not animate on all carriers

Important: File size limits are enforced by carriers, not just Twilio. For best compatibility across all carriers and devices, keep images under 500 KB. Images larger than carrier limits may cause delivery failures or be automatically compressed.

E.164 Number Format:

Always use E.164 format for phone numbers. The validation adds a basic check, and error messages guide users.

E.164 Format Examples:

CountryFormatExample
United States+1XXXXXXXXXX+14155552671
United Kingdom+44XXXXXXXXXX+447911123456
Australia+61XXXXXXXXX+61412345678
Germany+49XXXXXXXXXX+4915112345678

Trial Account Limitations:

If using a Twilio trial account:

  • You can only send messages to phone numbers you've verified in the console
  • Messages will include a trial account disclaimer
  • Upgrade to a paid account for production use

8. Implement Performance Optimizations

For this simple endpoint, performance bottlenecks are unlikely unless handling very high volume.

Expected baseline performance:

  • Single request latency: 500–2000 ms (depends on Twilio API response time)
  • Throughput: 50–100 requests per second (single instance)
  • Memory usage: ~50 MB baseline, +10 MB per 1000 concurrent requests

Asynchronous Operations:

Node.js is inherently asynchronous. Using async/await correctly ensures the server isn't blocked during the API call to Twilio.

Connection Pooling:

The Twilio SDK manages underlying HTTP connections. For extreme volume, ensure Node.js's default maxSockets is sufficient:

javascript
const http = require('http');
const https = require('https');

// Increase connection pool size for high-volume scenarios
http.globalAgent.maxSockets = 100;
https.globalAgent.maxSockets = 100;

Caching:

Not directly applicable for sending unique MMS messages. Caching might be relevant if:

  • Frequently sending the same image to different people (cache the image locally)
  • Looking up user data from a database before sending (cache database queries)

Load Testing:

Use tools to simulate traffic and identify bottlenecks:

bash
# Install k6
brew install k6  # macOS
# or download from https://k6.io/

# Create a test script (load-test.js)
javascript
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '30s', target: 10 },  // Ramp up to 10 users
        { duration: '1m', target: 10 },   // Stay at 10 users
        { duration: '30s', target: 0 },   // Ramp down
    ],
};

export default function () {
    const payload = JSON.stringify({
        to: '+14155552672',
        imageUrl: 'https://demo.twilio.com/owl.png',
        caption: 'Load test'
    });

    const params = {
        headers: {
            'Content-Type': 'application/json',
            'x-api-key': 'YOUR_API_KEY'
        },
    };

    const res = http.post('http://localhost:3000/send-mms', payload, params);

    check(res, {
        'status is 200': (r) => r.status === 200,
        'response time < 3000ms': (r) => r.timings.duration < 3000,
    });

    sleep(1);
}
bash
# Run the test
k6 run load-test.js

Profiling:

Use Node.js built-in profiler or specialized tools:

bash
# Node.js built-in profiler
node --prof index.js
# After stopping, process the log
node --prof-process isolate-*.log > processed.txt

# Or use Clinic.js for better visualization
npm install -g clinic
clinic doctor -- node index.js

9. Add Monitoring, Observability, and Analytics

Comprehensive Health Checks:

Implement detailed health checks that verify Twilio connectivity:

javascript
app.get('/health', async (req, res) => {
    const health = {
        status: 'UP',
        timestamp: new Date().toISOString(),
        uptime: process.uptime(),
        checks: {}
    };

    // Check Twilio connectivity (basic)
    try {
        // Simple check – verify SDK is initialized
        if (client) {
            health.checks.twilio = { status: 'UP', message: 'SDK initialized' };
        } else {
            health.checks.twilio = { status: 'DOWN', message: 'SDK not initialized' };
            health.status = 'DOWN';
        }
    } catch (error) {
        health.checks.twilio = { status: 'DOWN', message: error.message };
        health.status = 'DOWN';
    }

    // Check environment variables
    const requiredEnvVars = ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER'];
    const missingVars = requiredEnvVars.filter(v => !process.env[v]);

    if (missingVars.length > 0) {
        health.checks.config = {
            status: 'DOWN',
            message: `Missing env vars: ${missingVars.join(', ')}`
        };
        health.status = 'DOWN';
    } else {
        health.checks.config = { status: 'UP', message: 'All required env vars present' };
    }

    const statusCode = health.status === 'UP' ? 200 : 503;
    res.status(statusCode).json(health);
});

Performance Metrics:

Monitor event loop latency, CPU/memory usage, request latency, and error rates:

bash
npm install prom-client
javascript
const promClient = require('prom-client');

// Create a Registry
const register = new promClient.Registry();

// Add default metrics (CPU, memory, event loop lag)
promClient.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new promClient.Histogram({
    name: 'http_request_duration_seconds',
    help: 'Duration of HTTP requests in seconds',
    labelNames: ['method', 'route', 'status_code'],
    buckets: [0.1, 0.5, 1, 2, 5]
});

const mmsMessagesSent = new promClient.Counter({
    name: 'mms_messages_sent_total',
    help: 'Total number of MMS messages sent',
    labelNames: ['status']
});

register.registerMetric(httpRequestDuration);
register.registerMetric(mmsMessagesSent);

// Middleware to track request duration
app.use((req, res, next) => {
    const start = Date.now();

    res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        httpRequestDuration
            .labels(req.method, req.route?.path || req.path, res.statusCode)
            .observe(duration);
    });

    next();
});

// Update your /send-mms route to track success/failure
// In the success case:
mmsMessagesSent.labels('success').inc();

// In the error case:
mmsMessagesSent.labels('error').inc();

// Metrics endpoint
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
});

Error Tracking:

Integrate Sentry to capture, aggregate, and alert on runtime errors:

bash
npm install @sentry/node @sentry/tracing
javascript
// In index.js, initialize early
const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');

Sentry.init({
  dsn: "YOUR_SENTRY_DSN", // Get from Sentry project settings
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Tracing.Integrations.Express({ app }),
  ],
  tracesSampleRate: 0.1, // 10% of transactions for performance monitoring
  environment: process.env.NODE_ENV || 'development',
  release: 'twilio-mms-sender@' + process.env.npm_package_version,
});

// RequestHandler creates a separate execution context
app.use(Sentry.Handlers.requestHandler());
// TracingHandler creates a trace for every incoming request
app.use(Sentry.Handlers.tracingHandler());

// --- Your routes go here ---

// Error handler must be before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());

// Optional fallthrough error handler
app.use(function onError(err, req, res, next) {
  res.statusCode = 500;
  res.json({ success: false, error: 'Internal Server Error' });
});

Structured Logging:

Use Winston for production-grade logging:

bash
npm install winston
javascript
const winston = require('winston');

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    defaultMeta: { service: 'twilio-mms-sender' },
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
    ],
});

// Add console transport in non-production
if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.simple(),
    }));
}

// Replace console.log/error with logger
logger.info('Server starting up');
logger.error('Error occurred', { error: error.message, stack: error.stack });

Key Metrics to Track:

MetricPurposeAlert Threshold
Request latency (p50, p95, p99)API performancep95 > 3s
Error rateService health> 5%
MMS success rateDelivery effectiveness< 95%
Rate limit hitsCapacity planning> 10/min
Memory usageResource management> 80%
Event loop lagNode.js performance> 100ms

10. Troubleshoot Common Issues

Error: Permission Denied / Forbidden (403)

Diagnostic steps:

  1. Trial Account Limitation:

    • Log in to Twilio Console → Phone Numbers → Verified Caller IDs
    • Add the recipient number to your verified numbers
    • Verify the number with the SMS code sent
  2. Credential Issues:

    • Check TWILIO_ACCOUNT_SID matches console
    • Check TWILIO_AUTH_TOKEN matches console (not a test credential)
    • Verify credentials aren't expired
  3. Account Status:

    • Check your Twilio account balance and billing status
    • Verify your account is active and in good standing
  4. A2P 10DLC Registration (US only):

    • Navigate to Messaging → Regulatory Compliance in console
    • Complete A2P 10DLC registration if sending to US numbers
    • Wait for approval (can take 1-5 business days)

Error: Invalid Phone Number (21614)

Diagnostic steps:

  1. Number Format:

    • Verify to number uses E.164 format (e.g., +14155552671)
    • Verify from number (TWILIO_PHONE_NUMBER) uses E.164 format
    • Remove spaces, dashes, or parentheses
  2. Number Validation:

    • Use Twilio Lookup API to verify number validity
    • Check if the number is actually mobile (MMS requires mobile numbers)
  3. Country Code:

    • Ensure the country code is included (+ followed by country code)
    • US numbers start with +1, UK with +44, etc.

Error: Media Error (63016)

Diagnostic steps:

  1. Image URL Issues:

    • Test the URL in your browser – it should load the image
    • Try with curl: curl -I https://your-image-url.jpg
    • Ensure URL is publicly accessible (no authentication required)
    • Check URL doesn't have redirects or SSL certificate issues
    • Verify file extension (.jpg, .png, .gif)
  2. File Size:

    • Confirm file size is under 5 MB (preferably under 500 KB for best compatibility)
    • Use image compression tools to reduce size if needed
  3. Image Format:

    • Twilio supports JPEG, PNG, and GIF
    • Verify the Content-Type header is correct
    • Try a different image to isolate the issue
  4. Hosting Issues:

    • Check if your hosting service blocks automated requests
    • Ensure CORS headers allow Twilio's servers
    • Verify SSL certificate is valid

MMS Not Received

Diagnostic steps:

  1. Check Twilio Console:

    • Go to Monitor → Logs → Messaging
    • Find your message by SID
    • Check delivery status (sent, delivered, failed, undelivered)
  2. Recipient Device:

    • Verify device has MMS enabled in settings
    • Check cellular data or WiFi connection is active
    • Confirm device has sufficient storage
    • Try sending to a different number/device
  3. Carrier Issues:

    • Some carriers filter A2P (Application-to-Person) MMS
    • High-volume sending may trigger spam filters
    • Content filtering may block certain images
  4. Check Twilio Status:

Rate Limiting

Twilio rate limits (typical):

Account TypeMessages per SecondDaily Limit
Trial1 msg/sec200-500 msg/day
Free Tier1 msg/secVariable
Paid10+ msg/secVariable (based on trust score)
High VolumeCustomCustom

When you hit rate limits:

  • Twilio returns HTTP 429 (Too Many Requests)
  • Response includes error code 20429
  • Implement exponential backoff (see Section 4)
  • Contact Twilio to increase limits for your use case

Image Fetching Timeout

Twilio timeout for fetching images: Approximately 10 seconds.

Solutions:

  • Use a CDN for faster image delivery
  • Optimize image file sizes (compress before sending)
  • Host images on reliable infrastructure (AWS S3, Cloudinary)
  • Pre-warm CDN caches if sending to many recipients

General Troubleshooting Flowchart

┌─────────────────────────┐ │ MMS Send Failed? │ └────────┬────────────────┘ │ ├─→ Check HTTP status code │ ├─→ 400 Bad Request → Validate input format │ ├─→ 401 Unauthorized → Check API credentials │ ├─→ 403 Forbidden → Check account status & A2P registration │ ├─→ 21614 Invalid Number → Check phone number format │ ├─→ 63016 Media Error → Check image URL & size │ ├─→ 429 Rate Limit → Implement backoff/wait │ ├─→ 500 Server Error → Check Twilio status page │ └─→ No response → Check network connectivity

11. Deploy Your Application

Choose a Deployment Platform

PlatformBest ForPricingEase of Use
HerokuQuick prototypes, small appsFree tier available, paid plans start at $7/monthVery easy
RenderModern apps, containersFree tier available, paid plans start at $7/monthVery easy
RailwayDeveloper-friendly deployment$5 credit/month free, usage-basedVery easy
AWS EC2Full control, scalabilityPay-per-use, starts ~$5/monthModerate
AWS LambdaServerless, event-drivenFree tier, pay-per-invocationModerate
Google Cloud RunContainerized apps, autoscalingFree tier, pay-per-useModerate
DigitalOceanVPS hosting, predictable pricingStarts at $6/monthEasy

Deploy to Render (Example)

  1. Prepare Your Application:

    Add a start script to package.json:

    json
    {
      "name": "twilio-mms-sender",
      "version": "1.0.0",
      "scripts": {
        "start": "node index.js",
        "dev": "nodemon index.js"
      },
      "dependencies": {
        "express": "^4.18.2",
        "twilio": "^4.19.0",
        "dotenv": "^16.0.3"
      }
    }
  2. Create a render.yaml (optional):

    yaml
    services:
      - type: web
        name: twilio-mms-sender
        env: node
        buildCommand: npm install
        startCommand: npm start
        envVars:
          - key: NODE_ENV
            value: production
  3. Push to Git Repository:

    bash
    git init
    git add .
    git commit -m "Initial commit"
    git remote add origin YOUR_GIT_REPO_URL
    git push -u origin main
  4. Connect to Render:

    • Go to render.com and sign up
    • Click "New +" → "Web Service"
    • Connect your Git repository
    • Render auto-detects Node.js
    • Set environment variables (see next section)
    • Click "Create Web Service"

Configure Environment Variables

IMPORTANT: Never commit .env to version control. Configure environment variables in your deployment platform's settings.

Set these environment variables in your platform:

  • TWILIO_ACCOUNT_SID
  • TWILIO_AUTH_TOKEN
  • TWILIO_PHONE_NUMBER
  • PORT (usually auto-set by platform)
  • NODE_ENV=production

For Render:

  1. Navigate to your service in the Render dashboard
  2. Click "Environment" in the left sidebar
  3. Add each environment variable as a key-value pair
  4. Click "Save Changes"

For Heroku:

bash
heroku config:set TWILIO_ACCOUNT_SID=ACxxxxx
heroku config:set TWILIO_AUTH_TOKEN=your_token
heroku config:set TWILIO_PHONE_NUMBER=+14155552671

For AWS:

Use AWS Systems Manager Parameter Store or Secrets Manager:

bash
aws ssm put-parameter --name /twilio-mms/ACCOUNT_SID --value "ACxxxxx" --type SecureString

Docker Deployment (Optional)

Create a Dockerfile:

dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application files
COPY . .

# Expose port
EXPOSE 3000

# Start application
CMD ["node", "index.js"]

Create a .dockerignore:

node_modules npm-debug.log .env .git .gitignore *.md

Build and run:

bash
# Build image
docker build -t twilio-mms-sender .

# Run container (pass env vars)
docker run -p 3000:3000 \
  -e TWILIO_ACCOUNT_SID=your_sid \
  -e TWILIO_AUTH_TOKEN=your_token \
  -e TWILIO_PHONE_NUMBER=your_number \
  twilio-mms-sender

CI/CD with GitHub Actions

Create .github/workflows/deploy.yml:

yaml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Run linter
      run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v3

    - name: Deploy to Render
      env:
        RENDER_DEPLOY_HOOK: ${{ secrets.RENDER_DEPLOY_HOOK }}
      run: |
        curl -X POST $RENDER_DEPLOY_HOOK

Automated Testing Strategy:

Before deployment, run:

  1. Unit Tests: Test individual functions (validation, payload construction)
  2. Integration Tests: Test the full /send-mms endpoint with mocked Twilio API
  3. Linting: Check code style and potential bugs
  4. Security Scanning: Check for vulnerable dependencies

Example test with Jest:

bash
npm install --save-dev jest supertest
javascript
// __tests__/send-mms.test.js
const request = require('supertest');
const app = require('../index'); // Export your app from index.js

describe('POST /send-mms', () => {
    it('returns 400 if missing required fields', async () => {
        const res = await request(app)
            .post('/send-mms')
            .send({ to: '+14155552671' }); // Missing imageUrl

        expect(res.statusCode).toBe(400);
        expect(res.body.success).toBe(false);
    });

    it('returns 400 if phone number format is invalid', async () => {
        const res = await request(app)
            .post('/send-mms')
            .send({
                to: '415-555-2671', // Invalid format
                imageUrl: 'https://example.com/image.jpg'
            });

        expect(res.statusCode).toBe(400);
        expect(res.body.error).toContain('E.164 format');
    });
});

Add test script to package.json:

json
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest --coverage",
    "lint": "eslint ."
  }
}

Build Process

Ensure package.json and package-lock.json are committed. Your deployment platform will:

  1. Run npm ci (faster, more reliable than npm install)
  2. Start your application using npm start or the command in your Procfile

Example Procfile (for Heroku):

web: node index.js

Post-Deployment Checklist

  • Verify environment variables are set correctly
  • Test the /health endpoint: curl https://your-app.com/health
  • Send a test MMS: curl -X POST https://your-app.com/send-mms -H "Content-Type: application/json" -d '{"to":"+1234567890","imageUrl":"https://demo.twilio.com/owl.png"}'
  • Check application logs for errors
  • Set up monitoring alerts
  • Configure custom domain (if needed)
  • Enable HTTPS (usually automatic)
  • Update Twilio webhook URLs to point to production endpoints (if using webhooks)
  • Document your API for team members

Next Steps

Now that you have a working MMS sender, consider these enhancements:

Features:

  • Implement webhook handlers to track delivery status
  • Add support for sending to multiple recipients (batch sending)
  • Create message templates for common use cases
  • Implement message scheduling
  • Add support for other channels (WhatsApp, SMS)

Infrastructure:

  • Set up staging and production environments
  • Implement comprehensive logging and monitoring
  • Add automated testing and CI/CD
  • Configure horizontal scaling for high volume
  • Implement database integration for message tracking

Security:

  • Add API authentication and rate limiting
  • Implement role-based access control
  • Set up security monitoring and alerts
  • Conduct security audit
  • Implement credential rotation

Related Tutorials:

Resources:

Frequently Asked Questions

Why use Express.js with the Vonage API?

Express.js is lightweight and straightforward framework for creating API endpoints in Node.js. This allows you to quickly set up a server that handles requests to send MMS messages via the Vonage API.

When should I use MMS instead of SMS?

Use MMS when you need to send multimedia content like images. MMS allows for richer communication compared to text-only SMS, making it better for notifications, alerts, or marketing.

How to send MMS messages with Node.js?

Use the Vonage Messages API with Node.js and Express to send MMS. This involves setting up an Express server, integrating the Vonage Server SDK, and configuring your Vonage API credentials for MMS capability.

What is the Vonage Messages API used for?

The Vonage Messages API is a unified platform for sending messages across different channels like SMS, MMS, WhatsApp, and more. It simplifies the process of sending rich media messages programmatically.

How to set up Vonage API credentials?

Obtain your API Key and Secret from the Vonage Dashboard, create a Vonage Application with Messages capability, enable the Messages API, link your Vonage number, and store these credentials securely in a .env file.

What is the purpose of the private.key file?

The private.key file is crucial for authenticating your Vonage Application with the Messages API. Keep this file secure and never commit it to version control.

How do I handle Vonage API errors in Node.js?

Implement try...catch blocks around your Vonage API calls to handle potential errors during the MMS sending process. Check for status codes like 400, 403 and 500, as each of these can indicate a different type of issue.

Where can I find my Vonage API Key and Secret?

Your Vonage API Key and Secret are available in your Vonage API Dashboard, displayed at the top of the home page after logging in.

How to structure a Node.js project for sending MMS?

Create a project directory, initialize npm, install Express, the Vonage Server SDK, and dotenv. Set up a .gitignore file, and create index.js and .env files for your code and credentials respectively.

What is the purpose of a .gitignore file?

The .gitignore file specifies files and directories that should be excluded from version control (Git). It’s critical for preventing sensitive information, like your .env file, from being accidentally exposed publicly.

How to validate recipient phone numbers?

Use a regular expression or a validation library to verify that the recipient's phone number is in the correct E.164 format, such as +14155552671.

Why does MMS sending fail with a 403 error?

A 403 Forbidden error from the Vonage API usually means an authentication issue. This could be due to incorrect API keys, an invalid Application ID, an improperly configured private key, or the number not being linked to your application, or not being whitelisted if you're on a trial account. Also, ensure Messages API is selected as the default SMS handler in your Dashboard settings.

Can I send MMS messages internationally with Vonage?

Vonage's MMS sending capability has historically focused on US numbers. International MMS may not be supported or may fall back to SMS with a link to the image. Check Vonage’s official documentation for the most up-to-date information on international MMS support and any applicable restrictions.

What are some common troubleshooting tips for Vonage MMS?

Double-check your API credentials, ensure your image URL is publicly accessible, verify E.164 number formatting, check for Vonage number linking and MMS capability, confirm inbound/status URLs are set if required, and check whitelisting status if on a trial account.