code examples
code examples
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:
| Technology | Purpose | Why Use It |
|---|---|---|
| Node.js | JavaScript runtime for server-side applications | Asynchronous nature and large npm ecosystem |
| Express | Web application framework | Simple API endpoint creation |
| Twilio Programmable Messaging | SMS and MMS messaging API | Industry-leading deliverability and MMS support |
| dotenv | Environment variable management | Secure 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 (
curlor Postman)
System Architecture:
The system flow works as follows:
- Client sends a POST request to your Express app
- Express app uses Twilio SDK (configured with Auth Token and Account SID) to communicate with Twilio API
- Twilio API sends the MMS to the recipient's mobile device
- 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.
mkdir twilio-mms-sender
cd twilio-mms-senderInitialize Node.js Project:
Create a package.json file to manage dependencies. The -y flag accepts default settings.
npm init -yInstall Dependencies:
Install Express for the web server, the Twilio Node.js SDK to interact with the API, and dotenv to handle environment variables securely.
npm install express twilio dotenvWhat each package does:
express: Web framework for Node.jstwilio: Official Twilio library for Node.jsdotenv: Loads environment variables from.envintoprocess.env
Create Project Files:
touch index.js .env .gitignoreConfigure .gitignore:
Prevent committing sensitive credentials or unnecessary files to version control. Add this to your .gitignore file:
# Dependencies
node_modules
# Environment variables
.env
# Logs
*.log
# Operating system files
.DS_Store
Thumbs.dbWhy .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
- Log in to your Twilio Console
- Find your Account SID and Auth Token on the console dashboard
- Click the eye icon to reveal your Auth Token
- 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.
- Navigate to Phone Numbers → Manage → Buy a number in the Twilio Console
- Select your country (US numbers have best MMS support)
- Check the MMS capability filter to show only MMS-capable numbers
- Choose a number and click Buy
- 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.
- Navigate to Messaging → Regulatory Compliance in the Twilio Console
- Complete the A2P 10DLC Registration form
- Submit your business information for verification
- 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.
# Twilio API Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER=+14155552671
# Server Configuration
PORT=3000Variable explanations:
TWILIO_ACCOUNT_SID: Your Twilio Account SID from the consoleTWILIO_AUTH_TOKEN: Your Twilio Auth Token from the consoleTWILIO_PHONE_NUMBER: Your MMS-capable Twilio phone number in E.164 formatPORT: 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
// 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:
# 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:
| Section | Purpose |
|---|---|
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 Validation | Checks 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 Validation | Checks for presence and basic format of to and imageUrl. Uses the from number configured in .env. |
| Payload Construction | Creates 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 Handling | Logs the response and sends a 200 OK JSON response with the message SID and status. |
| Error Handling | Catches 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 Endpoint | A 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 Code | Meaning | Resolution |
|---|---|---|
| 21408 | Permission denied | Check account status, billing, or A2P 10DLC registration |
| 21610 | Unsubscribed recipient | Recipient has opted out; remove from list |
| 21614 | Invalid phone number | Use E.164 format (+14155552671) |
| 21211 | Invalid 'To' phone number | Check recipient number format |
| 63016 | Media error | Reduce 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:
npm install async-retryconst 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:
-- 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:
| Database | Best For | Considerations |
|---|---|---|
| PostgreSQL | Structured data, ACID compliance | Mature, feature-rich, good for complex queries |
| MongoDB | Flexible schema, document storage | NoSQL, good for rapid development |
| MySQL | Structured data, wide hosting support | Popular, well-documented |
| Redis | Caching, temporary data | Fast, 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:
npm install joiconst 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:
npm install express-rate-limitconst 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:
npm install rate-limit-redis redisconst 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:
// 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:
npm install corsconst 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:
| Service | Pros | Cons |
|---|---|---|
| AWS S3 | Reliable, scalable, global CDN | Requires AWS account, setup complexity |
| Cloudinary | Image optimization, transformations | Costs can increase with usage |
| Imgur | Simple, free tier available | Less control, may have restrictions |
| Your own CDN | Full control | Requires infrastructure management |
Supported Image Types and Sizes:
Twilio typically supports:
| Format | Max File Size | Notes |
|---|---|---|
| JPEG (.jpg, .jpeg) | 5 MB | Most widely supported |
| PNG (.png) | 5 MB | Supports transparency |
| GIF (.gif) | 5 MB | Animated 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:
| Country | Format | Example |
|---|---|---|
| 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:
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:
# Install k6
brew install k6 # macOS
# or download from https://k6.io/
# Create a test script (load-test.js)// 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);
}# Run the test
k6 run load-test.jsProfiling:
Use Node.js built-in profiler or specialized tools:
# 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.js9. Add Monitoring, Observability, and Analytics
Comprehensive Health Checks:
Implement detailed health checks that verify Twilio connectivity:
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:
npm install prom-clientconst 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:
npm install @sentry/node @sentry/tracing// 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:
npm install winstonconst 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:
| Metric | Purpose | Alert Threshold |
|---|---|---|
| Request latency (p50, p95, p99) | API performance | p95 > 3s |
| Error rate | Service health | > 5% |
| MMS success rate | Delivery effectiveness | < 95% |
| Rate limit hits | Capacity planning | > 10/min |
| Memory usage | Resource management | > 80% |
| Event loop lag | Node.js performance | > 100ms |
10. Troubleshoot Common Issues
Error: Permission Denied / Forbidden (403)
Diagnostic steps:
-
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
-
Credential Issues:
- Check
TWILIO_ACCOUNT_SIDmatches console - Check
TWILIO_AUTH_TOKENmatches console (not a test credential) - Verify credentials aren't expired
- Check
-
Account Status:
- Check your Twilio account balance and billing status
- Verify your account is active and in good standing
-
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:
-
Number Format:
- Verify
tonumber uses E.164 format (e.g., +14155552671) - Verify
fromnumber (TWILIO_PHONE_NUMBER) uses E.164 format - Remove spaces, dashes, or parentheses
- Verify
-
Number Validation:
- Use Twilio Lookup API to verify number validity
- Check if the number is actually mobile (MMS requires mobile numbers)
-
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:
-
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)
-
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
-
Image Format:
- Twilio supports JPEG, PNG, and GIF
- Verify the Content-Type header is correct
- Try a different image to isolate the issue
-
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:
-
Check Twilio Console:
- Go to Monitor → Logs → Messaging
- Find your message by SID
- Check delivery status (sent, delivered, failed, undelivered)
-
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
-
Carrier Issues:
- Some carriers filter A2P (Application-to-Person) MMS
- High-volume sending may trigger spam filters
- Content filtering may block certain images
-
Check Twilio Status:
- Visit Twilio Status Page
- Check for ongoing incidents or maintenance
Rate Limiting
Twilio rate limits (typical):
| Account Type | Messages per Second | Daily Limit |
|---|---|---|
| Trial | 1 msg/sec | 200-500 msg/day |
| Free Tier | 1 msg/sec | Variable |
| Paid | 10+ msg/sec | Variable (based on trust score) |
| High Volume | Custom | Custom |
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
| Platform | Best For | Pricing | Ease of Use |
|---|---|---|---|
| Heroku | Quick prototypes, small apps | Free tier available, paid plans start at $7/month | Very easy |
| Render | Modern apps, containers | Free tier available, paid plans start at $7/month | Very easy |
| Railway | Developer-friendly deployment | $5 credit/month free, usage-based | Very easy |
| AWS EC2 | Full control, scalability | Pay-per-use, starts ~$5/month | Moderate |
| AWS Lambda | Serverless, event-driven | Free tier, pay-per-invocation | Moderate |
| Google Cloud Run | Containerized apps, autoscaling | Free tier, pay-per-use | Moderate |
| DigitalOcean | VPS hosting, predictable pricing | Starts at $6/month | Easy |
Deploy to Render (Example)
-
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" } } -
Create a
render.yaml(optional):yamlservices: - type: web name: twilio-mms-sender env: node buildCommand: npm install startCommand: npm start envVars: - key: NODE_ENV value: production -
Push to Git Repository:
bashgit init git add . git commit -m "Initial commit" git remote add origin YOUR_GIT_REPO_URL git push -u origin main -
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_SIDTWILIO_AUTH_TOKENTWILIO_PHONE_NUMBERPORT(usually auto-set by platform)NODE_ENV=production
For Render:
- Navigate to your service in the Render dashboard
- Click "Environment" in the left sidebar
- Add each environment variable as a key-value pair
- Click "Save Changes"
For Heroku:
heroku config:set TWILIO_ACCOUNT_SID=ACxxxxx
heroku config:set TWILIO_AUTH_TOKEN=your_token
heroku config:set TWILIO_PHONE_NUMBER=+14155552671For AWS:
Use AWS Systems Manager Parameter Store or Secrets Manager:
aws ssm put-parameter --name /twilio-mms/ACCOUNT_SID --value "ACxxxxx" --type SecureStringDocker Deployment (Optional)
Create a 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:
# 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-senderCI/CD with GitHub Actions
Create .github/workflows/deploy.yml:
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_HOOKAutomated Testing Strategy:
Before deployment, run:
- Unit Tests: Test individual functions (validation, payload construction)
- Integration Tests: Test the full
/send-mmsendpoint with mocked Twilio API - Linting: Check code style and potential bugs
- Security Scanning: Check for vulnerable dependencies
Example test with Jest:
npm install --save-dev jest supertest// __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:
{
"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:
- Run
npm ci(faster, more reliable thannpm install) - Start your application using
npm startor the command in your Procfile
Example Procfile (for Heroku):
web: node index.js
Post-Deployment Checklist
- Verify environment variables are set correctly
- Test the
/healthendpoint: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:
- Send SMS with Node.js and Twilio
- Receive and reply to incoming messages
- Track message delivery with webhooks
- Build SMS campaigns with Node.js
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.