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.
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 (curl or 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.
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:
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.
.env
# 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.jsrequire('dotenv').config(); // Load environment variables from .env fileconst 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 variablesconst 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 Validationconst { to, imageUrl, caption } = req.body;
constfrom = process.env.TWILIO_PHONE_NUMBER; // Sender number from .envif (!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 {
newURL(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 Twilioconst mmsPayload = {
body: caption || '', // Optional text captionfrom: 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 possiblelet 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 codesif (error.code === 21408) {
errorMessage = 'Permission denied: Check your Twilio account status or number capabilities.';
statusCode = 403;
} elseif (error.code === 21614) {
errorMessage = 'Invalid phone number format. Use E.164 format (e.g., +14155552671).';
statusCode = 400;
} elseif (error.code === 21610) {
errorMessage = 'Unsubscribed recipient: The recipient has opted out of messages from this number.';
statusCode = 400;
} elseif (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;
}
} elseif (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 ----constPORT = 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:
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:
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 = awaitretry(async bail => {
// bail is a function to stop retrying for non-recoverable errorstry {
console.log('Calling client.messages.create...'); // Log each attemptconst 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(newError(`Non-retriable Twilio error: ${error.code}`));
return;
}
// For other errors (5xx, network issues, 429), throw to trigger retrythrow error;
}
}, {
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 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 retriesconsole.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 exampleCREATE TABLE mms_messages (
id SERIAL PRIMARY KEY,
message_sid VARCHAR(255) UNIQUENOT 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 TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP
);
CREATE TABLE delivery_receipts (
id SERIAL PRIMARY KEY,
message_sid VARCHAR(255) REFERENCES mms_messages(message_sid),
status VARCHAR(50) NOT NULL,
timestampTIMESTAMPNOT 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);
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
constJoi = require('joi');
// Define validation schemaconst 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 handlerconst { 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 minutesmax: 100, // Limit each IP to 100 requests per windowMsstandardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: false, // Disable the `X-RateLimit-*` headersmessage: '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);
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)
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:
const promClient = require('prom-client');
// Create a Registryconst register = new promClient.Registry();
// Add default metrics (CPU, memory, event loop lag)
promClient.collectDefaultMetrics({ register });
// Custom metricsconst 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 earlyconstSentry = require('@sentry/node');
constTracing = require('@sentry/tracing');
Sentry.init({
dsn: "YOUR_SENTRY_DSN", // Get from Sentry project settingsintegrations: [
newSentry.Integrations.Http({ tracing: true }),
newTracing.Integrations.Express({ app }),
],
tracesSampleRate: 0.1, // 10% of transactions for performance monitoringenvironment: 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(functiononError(err, req, res, next) {
res.statusCode = 500;
res.json({ success: false, error: 'Internal Server Error' });
});
name:DeploytoProductionon:push:branches: [ main ]
jobs:test:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3-name:SetupNode.jsuses:actions/setup-node@v3with:node-version:'18'cache:'npm'-name:Installdependenciesrun:npmci-name:Runtestsrun:npmtest-name:Runlinterrun:npmrunlintdeploy:needs:testruns-on:ubuntu-latestif:github.ref=='refs/heads/main'steps:-uses:actions/checkout@v3-name:DeploytoRenderenv:RENDER_DEPLOY_HOOK:${{secrets.RENDER_DEPLOY_HOOK}}run:|
curl -X POST $RENDER_DEPLOY_HOOK
Automated Testing Strategy:
Before deployment, run:
Unit Tests: Test individual functions (validation, payload construction)
Integration Tests: Test the full /send-mms endpoint with mocked Twilio API
Linting: Check code style and potential bugs
Security Scanning: Check for vulnerable dependencies
Example test with Jest:
Bash
npm install --save-dev jest supertest
JavaScript
// __tests__/send-mms.test.jsconst request = require('supertest');
const app = require('../index'); // Export your app from index.jsdescribe('POST /send-mms', () => {
it('returns 400 if missing required fields', async () => {
const res = awaitrequest(app)
.post('/send-mms')
.send({ to: '+14155552671' }); // Missing imageUrlexpect(res.statusCode).toBe(400);
expect(res.body.success).toBe(false);
});
it('returns 400 if phone number format is invalid', async () => {
const res = awaitrequest(app)
.post('/send-mms')
.send({
to: '415-555-2671', // Invalid formatimageUrl: 'https://example.com/image.jpg'
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toContain('E.164 format');
});
});
Ensure package.json and package-lock.json are committed. Your deployment platform will:
Run npm ci (faster, more reliable than npm install)
Start your application using npm start or the command in your Procfile
Example Procfile (for Heroku):
Code
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)
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.
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.
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.
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.
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.
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.
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.
Your Vonage API Key and Secret are available in your Vonage API Dashboard, displayed at the top of the home page after logging in.
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.
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.
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.
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.
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.
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.
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:
Prerequisites:
curlor Postman)System Architecture:
The system flow works as follows:
Final Outcome: A running Express server with a
/send-mmsendpoint 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.jsonfile to manage dependencies. The-yflag accepts default settings.Install Dependencies:
Install Express for the web server, the Twilio Node.js SDK to interact with the API, and
dotenvto handle environment variables securely.What each package does:
express: Web framework for Node.jstwilio: Official Twilio library for Node.jsdotenv: Loads environment variables from.envintoprocess.envCreate Project Files:
touch index.js .env .gitignoreConfigure
.gitignore:Prevent committing sensitive credentials or unnecessary files to version control. Add this to your
.gitignorefile: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:
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
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.
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.
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
.envfile and add your credentials. Replace placeholder values with your actual credentials.Variable 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 on3. 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:
require('dotenv').config().envintoprocess.env. Call this early.express()app.use(express.json())req.body.twilio(accountSid, authToken)process.env.app.post('/send-mms', ...)asyncfunction to useawaitfor the asynchronousclient.messages.createcall.toandimageUrl. Uses thefromnumber configured in.env.mmsPayloadobject withbody,from,to, andmediaUrlarray.client.messages.create(mmsPayload)200 OKJSON response with the message SID and status./healthEndpointapp.listen(...)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-mmsroute implements error handling usingtry...catch. It distinguishes between:Specific error messages are extracted from Twilio error codes to help you diagnose issues quickly.
Common Twilio Error Codes:
Logging:
Basic logging using
console.logandconsole.errorshows request reception, payload, API responses, and errors. For production, use a dedicated logging library like Winston or Pino for: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:
When NOT to retry:
Example with
async-retry: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:
5. Database Integration (Optional)
This guide focuses on the API endpoint for sending MMS. A production application would likely require a database to:
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:
ORMs to Consider:
6. Add Security Features
Secrets Management:
Handle credentials via
.envand.gitignore. Never commit.env. Use environment variables provided by your deployment platform in production (see Section 11).Best practices:
Input Validation:
Basic validation is implemented for
toandimageUrl. Enhance this as needed: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:
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:
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:
// 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:
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:
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:
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
imageUrlmust be publicly accessible over the internet for Twilio to fetch and attach it. URLs behind firewalls or requiring authentication will fail.Recommended image hosting:
Supported Image Types and Sizes:
Twilio typically supports:
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:
Trial Account Limitations:
If using a Twilio trial account:
8. Implement Performance Optimizations
For this simple endpoint, performance bottlenecks are unlikely unless handling very high volume.
Expected baseline performance:
Asynchronous Operations:
Node.js is inherently asynchronous. Using
async/awaitcorrectly 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
maxSocketsis 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:
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:
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:
// 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:
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:
10. Troubleshoot Common Issues
Error: Permission Denied / Forbidden (403)
Diagnostic steps:
Trial Account Limitation:
Credential Issues:
TWILIO_ACCOUNT_SIDmatches consoleTWILIO_AUTH_TOKENmatches console (not a test credential)Account Status:
A2P 10DLC Registration (US only):
Error: Invalid Phone Number (21614)
Diagnostic steps:
Number Format:
tonumber uses E.164 format (e.g., +14155552671)fromnumber (TWILIO_PHONE_NUMBER) uses E.164 formatNumber Validation:
Country Code:
Error: Media Error (63016)
Diagnostic steps:
Image URL Issues:
curl:curl -I https://your-image-url.jpgFile Size:
Image Format:
Hosting Issues:
MMS Not Received
Diagnostic steps:
Check Twilio Console:
Recipient Device:
Carrier Issues:
Check Twilio Status:
Rate Limiting
Twilio rate limits (typical):
When you hit rate limits:
Image Fetching Timeout
Twilio timeout for fetching images: Approximately 10 seconds.
Solutions:
General Troubleshooting Flowchart
11. Deploy Your Application
Choose a Deployment Platform
Deploy to Render (Example)
Prepare Your Application:
Add a start script to
package.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):services: - type: web name: twilio-mms-sender env: node buildCommand: npm install startCommand: npm start envVars: - key: NODE_ENV value: productionPush to Git Repository:
git init git add . git commit -m "Initial commit" git remote add origin YOUR_GIT_REPO_URL git push -u origin mainConnect to Render:
Configure Environment Variables
IMPORTANT: Never commit
.envto 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=productionFor Render:
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:Create a
.dockerignore: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:
/send-mmsendpoint with mocked Twilio APIExample test with Jest:
// __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.jsonandpackage-lock.jsonare committed. Your deployment platform will:npm ci(faster, more reliable thannpm install)npm startor the command in your ProcfileExample
Procfile(for Heroku):Post-Deployment Checklist
/healthendpoint:curl https://your-app.com/healthcurl -X POST https://your-app.com/send-mms -H "Content-Type: application/json" -d '{"to":"+1234567890","imageUrl":"https://demo.twilio.com/owl.png"}'Next Steps
Now that you have a working MMS sender, consider these enhancements:
Features:
Infrastructure:
Security:
Related Tutorials:
Resources: