How to Send MMS with Sinch API and Node.js: Complete 2025 Developer Guide
Learn how to send MMS messages with Sinch API, Node.js, and Express. Complete tutorial with media handling, fallback SMS, error handling, and production deployment best practices for multimedia messaging.
You'll build a production-ready Node.js application using the Express framework to send Multimedia Messaging Service (MMS) messages via the Sinch MMS JSON API. This guide covers everything from initial project setup to deployment and monitoring.
Important: Sinch offers multiple APIs (e.g., legacy APIs, unified Messages API). This guide focuses on a specific MMS JSON API structure. Always verify endpoints, payload structures, authentication methods, and specific behaviors against the current, official Sinch documentation relevant to your account and the specific API product version you are using. API details can change.
By the end of this tutorial, you will have a functional Express API endpoint capable of accepting MMS details (recipient number, media URLs, text) and dispatching the message through Sinch, including handling fallbacks to SMS.
Project Overview and Goals
Goal: To create a robust backend service that enables sending MMS messages programmatically using Sinch's dedicated MMS API.
Problem Solved: Provides a reliable way to integrate rich media messaging into applications, automating communication workflows that require images, videos, or other media alongside text. This is essential for marketing campaigns, transactional notifications with visual content, and customer engagement workflows that need multimedia support.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications.
Express: A minimal and flexible Node.js web application framework used to build the API layer.
Axios: A promise-based HTTP client for making requests to the Sinch API.
dotenv: A module to load environment variables from a .env file for secure credential management.
Sinch MMS JSON API: The specific Sinch API endpoint and protocol for sending MMS messages (subject to verification against current documentation).
(Note: This is a simplified text-based representation.)
Prerequisites:
Node.js and npm (or Yarn) installed. As of January 2025, Node.js 22 is the current LTS version (codenamed 'Jod') recommended for production use. Node.js 18.x reaches end-of-life on April 30, 2025 – plan to upgrade to Node.js 20 or 22 for continued support.
Note: This guide uses Express v5.x (released October 2024) which requires Node.js 18+. Express 5 includes security improvements, async error handling, and drops support for Node.js versions before v18.
Note: This guide uses Axios v1.12.x for HTTP requests. Current versions include improved fetch adapter support and enhanced timeout configuration.
A Sinch account with access to the MMS API product you intend to use.
Your Sinch Service Plan ID (may also be called Campaign ID or similar - verify term in your dashboard/docs).
Your Sinch API Token associated with the Service Plan ID.
A Sinch provisioned phone number (Short Code, Toll-Free, or 10DLC) capable of sending MMS.
Publicly accessible URLs for the media files (images, videos, audio, etc.) you intend to send. These servers must return a Content-Length header.
Familiarity with the official Sinch MMS API documentation for verification.
How to Set Up a Node.js Project for Sinch MMS Integration
Initialize your Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
Bash
mkdir sinch-mms-sender
cd sinch-mms-sender
Step 2: Initialize Node.js Project
Initialize the project using npm. Accept the defaults or customize as needed.
Bash
npm init -y
This creates a package.json file.
Step 3: Install Dependencies
We need Express for the server, Axios to make HTTP requests, and dotenv to handle environment variables. We'll also add nodemon as a development dependency for easier development.
Create a file named .env in the root of your project. This file will store your sensitive credentials and configuration. Never commit this file to version control.
Text
# .env
# --- Sinch Credentials ---
# Get these from your Sinch Customer Dashboard (e.g., under SMS -> APIs)
# Replace placeholders with your actual values.
# Your Service Plan ID (or Campaign ID - verify term)
SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
# Your API Token for the Service Plan ID
SINCH_API_TOKEN=YOUR_API_TOKEN
# Your provisioned Sinch Number (use E.164 format, e.g., +1xxxxxxxxxx)
SINCH_NUMBER=YOUR_SINCH_PHONE_NUMBER
# --- Sinch API Configuration ---
# CRITICAL: Verify the correct API base URL and structure in the official Sinch documentation
# for your specific region (us, eu, au, etc.) and API product version.
# The structure might require the service plan ID in the path as shown, or it might be different.
# Example structure (VERIFY THIS):
SINCH_API_BASE_URL=https://us.mms.api.sinch.com/v1/services/YOUR_SERVICE_PLAN_ID
# --- Server Configuration ---
# Port your Express server will listen on
PORT=3000
# Optional: Logging level (e.g., 'info', 'debug', 'warn', 'error')
# LOG_LEVEL=info
SINCH_SERVICE_PLAN_ID: Found on your Sinch Customer Dashboard. The exact name and location might vary. Replace YOUR_SERVICE_PLAN_ID with your actual ID.
SINCH_API_TOKEN: Found on the Sinch Customer Dashboard, often near the Service Plan ID configuration. Replace YOUR_API_TOKEN with your actual token.
SINCH_NUMBER: A virtual number assigned to your Sinch account, capable of sending MMS. Use the E.164 international format (e.g., +12223334444). Replace YOUR_SINCH_PHONE_NUMBER with your actual Sinch number.
SINCH_API_BASE_URL: The base URL for the Sinch MMS API. Crucially, verify the correct URL and structure in the official Sinch documentation. It can vary based on region, API version, and whether the Service Plan ID is part of the path. The example provided is common but requires verification. Replace YOUR_SERVICE_PLAN_ID within the URL if this structure is correct for your API.
PORT: The port your Express server will listen on.
Step 5: Set Up Project Structure
Create the following basic structure:
Text
sinch-mms-sender/
├── node_modules/
├── public/ # (Optional: for static files if needed)
├── src/
│ ├── controllers/ # Request/response handling
│ │ └── mmsController.js
│ ├── services/ # Business logic (Sinch interaction)
│ │ └── sinchService.js
│ ├── routes/ # API route definitions
│ │ └── mmsRoutes.js
│ ├── app.js # Express app configuration
│ └── server.js # Server entry point
├── .env # Environment variables (DO NOT COMMIT)
├── .gitignore # Git ignore file
├── package.json
└── package-lock.json
Create the src directory and the subdirectories/files within it.
Step 6: Create .gitignore
Create a .gitignore file in the project root to prevent committing sensitive files and unnecessary directories.
{"scripts":{"start":"node src/server.js","dev":"nodemon src/server.js","test":"echo \"Error: no test specified\" && exit 1"}}
(Note: The test script is a placeholder. It will be updated in Section 13 when testing with Jest is introduced.)
npm start: Runs the application using Node.
npm run dev: Runs the application using nodemon, which automatically restarts the server on file changes.
How to Implement the Sinch MMS Service Layer
This service will encapsulate the logic for interacting with the Sinch MMS API.
File:src/services/sinchService.js
JavaScript
// src/services/sinchService.jsconst axios = require('axios');
// Load environment variablesrequire('dotenv').config();
constSINCH_SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID;
constSINCH_API_TOKEN = process.env.SINCH_API_TOKEN;
constSINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL; // Assumes base URL includes service ID path per .env exampleif (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN || !SINCH_API_BASE_URL) {
console.error("Error: Missing Sinch credentials or API base URL in .env file. Please check SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, and SINCH_API_BASE_URL.");
process.exit(1); // Stop the application if critical config is missing
}
// Construct the specific endpoint for the sendmms action// **VERIFY THIS ENDPOINT STRUCTURE** against the official Sinch MMS JSON API documentation for your account/region.// This assumes the base URL already contains '/v1/services/{service_plan_id}'constSEND_MMS_ENDPOINT = `${SINCH_API_BASE_URL}/actions/sendmms`;
/**
* Sends an MMS message using the Sinch MMS JSON API.
*
* @param {string} to The recipient's phone number in E.164 international format (e.g., +1...).
* @param {string} from The Sinch phone number sending the message (E.164 format).
* @param {string} subject The subject line for the MMS (max 80 chars, 40 recommended).
* @param {Array<object>} slides An array of slide objects. Structure MUST match Sinch API docs.
* Each slide can contain text and/or one media type (image, audio, video, etc.).
* @param {string} [fallbackText] Optional text for the fallback SMS if MMS fails or is disabled. Required if fallback isn't explicitly disabled.
* @param {object} [options] Optional parameters like clientReference, disableFallbackSms, etc. (Refer to Sinch Docs).
* @returns {Promise<object>} The success response data from the Sinch API (contains tracking-id).
* @throws {Error} If the API request fails or returns an error status.
*/constsendMms = async (to, from, subject, slides, fallbackText, options = {}) => {
console.log(`Attempting to send MMS to ${to} from ${from}`);
// Basic validationif (!to || !from || !subject || !Array.isArray(slides) || slides.length === 0) {
thrownewError('Missing required parameters: to, from, subject, or non-empty slides array.');
}
if (!to.startsWith('+') || !from.startsWith('+')) {
console.warn(`Warning: Phone numbers should be in E.164 format (start with +). To: ${to}, From: ${from}`);
// Consider throwing an error here in production for strict validation
}
if (slides.length > 8) {
console.warn('Warning: Maximum of 8 slides recommended per Sinch guidelines.');
}
// Ensure fallbackText is provided if fallback SMS is enabled (default behavior)const disableFallbackSms = options.disableFallbackSms === true;
if (!disableFallbackSms && !fallbackText) {
// Fallback is enabled by default, so text is required unless explicitly disabled.thrownewError('fallbackText is required when disableFallbackSms is false (or not provided).');
}
// **VERIFY PAYLOAD STRUCTURE** against official Sinch MMS JSON API documentation.const payload = {
action: 'sendmms',
'service-id': SINCH_SERVICE_PLAN_ID, // May or may not be required depending on API structure/endpoint usedto: to,
from: from,
'message-subject': subject,
slide: slides, // Array of slide objects - structure is critical// --- Optional Parameters (Verify names and values in Sinch Docs) ---'fallback-sms-text': disableFallbackSms ? undefined : fallbackText, // Only include if fallback is enabled'disable-fallback-sms': options.disableFallbackSms || false, // Default: false (fallback enabled)'disable-fallback-sms-link': options.disableFallbackSmsLink || false, // Default: false// 'fallback-sms-link-expiration': options.fallbackLinkExpiration, // Example: ISO8601 Date String// 'mms-expiry-timestamp': options.mmsExpiryTimestamp, // Example: ISO8601 Date String'client-reference': options.clientReference, // Your internal reference ID (max 64 chars recommended)'cache-content': options.cacheContent === undefined ? true : options.cacheContent, // Default: true (Sinch may cache media)
};
// Remove undefined optional keys to keep payload cleanObject.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]);
console.log('Constructed Sinch Payload:', JSON.stringify(payload, null, 2)); // Log payload for debuggingtry {
// **VERIFY AUTHENTICATION METHOD** (Bearer Token is common, but check Sinch Docs)const response = await axios.post(SEND_MMS_ENDPOINT, payload, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${SINCH_API_TOKEN}`// Standard Bearer token auth
},
timeout: 15000// Set a reasonable timeout (e.g., 15 seconds) for the API call
});
console.log('Sinch API Response Status:', response.status);
console.log('Sinch API Response Data:', JSON.stringify(response.data, null, 2));
// Sinch often returns 200 OK even for queued messages, but check response body for success indicator if available// The exact success condition might vary based on the API version. This checks for a common pattern.if (response.data && response.data.status === 'success' && response.data['tracking-id']) {
console.log(`MMS successfully queued with Sinch. Tracking ID: ${response.data['tracking-id']}`);
return response.data; // Contains tracking-id etc.
} else {
// Handle cases where Sinch might return 200 but indicate failure/issue in the body, or success structure differsconsole.error('Sinch API response body indicates potential issue:', response.data);
thrownewError(`Sinch API did not return expected success status or tracking-id in response body: ${JSON.stringify(response.data)}`);
}
} catch (error) {
console.error('Error sending MMS via Sinch:', error.message);
let errorMessage = 'Failed to send MMS via Sinch API.';
if (error.response) {
// The request was made and the server responded with a status code outside the 2xx rangeconsole.error('Sinch API Error Status:', error.response.status);
console.error('Sinch API Error Headers:', error.response.headers);
console.error('Sinch API Error Data:', JSON.stringify(error.response.data, null, 2));
// Include details from Sinch error response if availableconst errorDetails = error.response.data ? `: ${JSON.stringify(error.response.data)}` : '';
errorMessage = `Sinch API request failed with status ${error.response.status}${errorDetails}`;
} elseif (error.request) {
// The request was made but no response was receivedconsole.error('Sinch API No Response Received. Request details:', error.request);
errorMessage = 'No response received from Sinch API. Check network connectivity and endpoint URL.';
} else {
// Something happened in setting up the request that triggered an Errorconsole.error('Axios Request Setup Error:', error.message);
errorMessage = `Error setting up request to Sinch API: ${error.message}`;
}
// Re-throw a more informative error to be handled by the controllerthrownewError(errorMessage);
}
};
module.exports = {
sendMms,
};
Explanation:
Dependencies & Config: Imports axios and dotenv. Loads environment variables and checks for essential Sinch credentials and the base URL, exiting if missing.
Endpoint Construction: Defines the full URL for the sendmms action based on the SINCH_API_BASE_URL. Includes a prominent warning to verify this endpoint against official documentation.
sendMms Function:
Takes required parameters (to, from, subject, slides) and optional fallbackText and options.
Validates that fallbackText is present if fallback SMS is not explicitly disabled.
Constructs the JSON payload according to a common Sinch MMS JSON API specification. Includes a warning to verify the payload structure against official documentation. Handles common optional fields and cleans undefined values.
Uses axios.post to send the request.
Sets necessary headers: Content-Type and Authorization (Bearer token - verify this method).
Includes robust error handling for different failure scenarios (API errors with status codes, network errors, request setup errors), logging detailed information.
Logs the constructed payload and the received response for debugging.
Checks the response body for a success indicator (e.g., status: 'success' and tracking-id) as 200 OK might not guarantee acceptance. This check might need adjustment based on the specific API response structure.
Returns the success response data from Sinch upon successful queueing.
Throws detailed errors for upstream handling.
How to Build Express API Routes and Controllers for MMS
Now, let's create the Express route and controller to expose our MMS sending functionality.
File:src/routes/mmsRoutes.js
JavaScript
// src/routes/mmsRoutes.jsconst express = require('express');
const mmsController = require('../controllers/mmsController');
// Optional: Import validation middleware if using express-validator (see Section 7)// const { validateSendMms } = require('../middleware/validators'); // Example pathconst router = express.Router();
// Define the route for sending MMS// POST /api/mms/send// Optional: Add validation middleware before the controller// router.post('/send', validateSendMms, mmsController.handleSendMms);
router.post('/send', mmsController.handleSendMms);
module.exports = router;
File:src/controllers/mmsController.js
JavaScript
// src/controllers/mmsController.jsconst sinchService = require('../services/sinchService');
// Load environment variables to get the sender numberrequire('dotenv').config();
constSINCH_NUMBER = process.env.SINCH_NUMBER;
if (!SINCH_NUMBER) {
console.error("FATAL: SINCH_NUMBER is not defined in .env file. Cannot determine sender number.");
process.exit(1);
}
/**
* Handles the incoming API request to send an MMS.
* Expects request body adhering to the defined contract, e.g.:
* {
* "to": "+1xxxxxxxxxx", // E.164 format
* "subject": "Your Media Update", // Max 80 chars
* "slides": [ // 1-8 slides, structure defined by Sinch API docs
* { "image": { "url": "https://your-cdn.com/image.jpg" }, "message-text": "Check out this picture!" },
* { "video": { "url": "https://your-cdn.com/video.mp4" }, "message-text": "And this video too." }
* ],
* "fallbackText": "View your media update here: [link inserted by Sinch if enabled]", // Required if fallback not disabled
* "clientReference": "internal-order-123", // Optional, max 64 chars
* "disableFallbackSms": false, // Optional, default false
* "disableFallbackSmsLink": false // Optional, default false
* }
*/consthandleSendMms = async (req, res) => {
// Extract data from request bodyconst { to, subject, slides, fallbackText, clientReference,
disableFallbackSms, disableFallbackSmsLink /* Add other options if needed */ } = req.body;
constfrom = SINCH_NUMBER; // Use the number configured in .env as the sender// Basic Input Validation (Strongly recommend using a library like express-validator for production - see Section 7)if (!to || !subject || !Array.isArray(slides) || slides.length === 0) {
console.warn(`Validation failed for incoming request: Missing required fields.`);
return res.status(400).json({
status: 'error',
message: 'Bad Request: Missing required fields: `to`, `subject`, `slides` (must be a non-empty array).',
});
}
// Add more specific validation here or use middleware (e.g., check 'to' format, subject length, slide content structure)// Prepare options object for the service layerconst serviceOptions = {
clientReference,
disableFallbackSms,
disableFallbackSmsLink,
// Pass through other relevant options extracted from req.body
};
try {
console.log(`Processing request to send MMS via Sinch: To=${to}, From=${from}, Subject=${subject}`);
const result = await sinchService.sendMms(
to,
from,
subject,
slides,
fallbackText, // Pass fallbackText explicitly
serviceOptions // Pass the options bundle
);
// If sendMms resolves, it means the request was accepted by Sinch (or seemed to be)console.log(`Successfully queued MMS to ${to} via Sinch. Tracking ID: ${result['tracking-id']}`);
// Respond with the success status and the data received from Sinch (including tracking ID)
res.status(200).json({
status: 'success',
message: 'MMS request accepted by Sinch and queued for delivery.',
data: result, // Send back the full Sinch success response
});
} catch (error) {
// Log the detailed error caught from the service layerconsole.error(`Failed to process MMS request for ${to}:`, error.message); // error.message now contains detailed info// Determine appropriate status code based on error type if possible// If the error came from Sinch API (indicated by the service layer error message), use 400 or a more specific code if known.// Otherwise, use 500 for internal server errors.const statusCode = error.message.includes("Sinch API request failed") || error.message.includes("required parameters") || error.message.includes("fallbackText is required") ? 400 : 500;
res.status(statusCode).json({
status: 'error',
message: 'Failed to send MMS.',
// Provide the specific error message back (be cautious about leaking too much detail in production)error: error.message,
});
}
};
module.exports = {
handleSendMms,
};
Explanation:
Routes (mmsRoutes.js): Defines a single POST route /api/mms/send that maps to the handleSendMms controller function. Includes a commented-out example showing where validation middleware (Section 7) would go.
Controller (mmsController.js):
Imports the sinchService.
Loads the SINCH_NUMBER from environment variables to use as the from number, exiting if not found.
Extracts expected fields (to, subject, slides, fallbackText, and optional fields like clientReference, disableFallbackSms) from the request body (req.body).
Performs basic validation on required fields. Strongly recommends using express-validator (Section 7) for robust validation in production.
Bundles optional parameters into a serviceOptions object.
Calls sinchService.sendMms with the extracted and validated data.
Handles success: Logs the success and responds with a 200 status and the Sinch API success response (which should include the tracking-id).
Handles errors: Catches errors thrown by the service layer, logs the detailed error message, determines an appropriate HTTP status code (400 for client-side/API input errors, 500 for internal server issues), and responds with a JSON error message including the error details from the service.
How to Configure Express Application with Sinch MMS Integration
Let's tie everything together in our main Express application file.
File:src/app.js
JavaScript
// src/app.jsconst express = require('express');
const mmsRoutes = require('./routes/mmsRoutes');
// Optional: Import security middleware like rate limiter (see Section 7)// const { apiLimiter } = require('./middleware/rateLimiter');// Optional: Import request logger like pino-http (see Section 5)// const { httpLogger } = require('./utils/logger');// Load environment variables early, especially for PORT and NODE_ENVrequire('dotenv').config();
const app = express();
// --- Core Middlewares ---// Request Logging (Replace with a structured logger like pino in production - See Section 5)// app.use(httpLogger); // Example using pino-http
app.use((req, res, next) => {
console.log(`[${newDate().toISOString()}] Incoming Request: ${req.method}${req.originalUrl}`);
next(); // Pass control to the next middleware/route handler
});
// JSON Body Parser: Crucial for reading req.body from JSON payloads
app.use(express.json({ limit: '1mb' })); // Set a reasonable payload size limit// URL-encoded Body Parser (optional, if you expect form data)
app.use(express.urlencoded({ extended: true }));
// --- Security Middlewares (Examples - See Section 7) ---// app.use('/api/', apiLimiter); // Apply rate limiting to API routes// Add other security middleware here (helmet, cors if needed)// --- API Routes ---
app.use('/api/mms', mmsRoutes); // Mount the MMS routes under /api/mms// --- Health Check Endpoint ---
app.get('/health', (req, res) => {
// Basic health check, can be expanded later
res.status(200).json({ status: 'ok', timestamp: newDate().toISOString() });
});
// --- Error Handling Middlewares (Must be defined LAST) ---// Catch-all for 404 Not Found errors (requests falling through routes)
app.use((req, res, next) => {
res.status(404).json({ status: 'error', message: `Resource not found: ${req.originalUrl}` });
});
// Global Error Handler: Catches errors passed via next(error) or thrown in async route handlers// Needs 4 arguments to be recognized by Express as an error handler
app.use((err, req, res, next) => {
// Log the error internally (use a proper logger in production)console.error("Unhandled Error:", err.stack || err);
// Determine status code: Use error's status if set, otherwise default to 500const statusCode = typeof err.status === 'number' ? err.status : 500;
// Send generic error response to the client
res.status(statusCode).json({
status: 'error',
message: err.message || 'Internal Server Error',
// Optionally include stack trace in development mode ONLY
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
module.exports = app;
File:src/server.js
JavaScript
// src/server.jsconst app = require('./app');
// Ensure environment variables are loaded (dotenv.config() might be called in app.js already)require('dotenv').config();
constPORT = process.env.PORT || 3000; // Use PORT from .env or default to 3000constNODE_ENV = process.env.NODE_ENV || 'development';
const server = app.listen(PORT, () => { // Store server instance for graceful shutdownconsole.log(`Server running in ${NODE_ENV} mode on port ${PORT}`);
console.log("--- Configuration Check ---");
console.log(`Sinch Service Plan ID configured: ${process.env.SINCH_SERVICE_PLAN_ID ? 'Yes' : 'NO - Check .env!'}`);
console.log(`Sinch API Token configured: ${process.env.SINCH_API_TOKEN ? 'Yes' : 'NO - Check .env!'}`);
console.log(`Sinch Sender Number configured: ${process.env.SINCH_NUMBER || 'NO - Check .env!'}`);
console.log(`Sinch API Base URL configured: ${process.env.SINCH_API_BASE_URL || 'NOT SET - Check .env!'}`);
console.log("---------------------------");
if (!process.env.SINCH_SERVICE_PLAN_ID || !process.env.SINCH_API_TOKEN || !process.env.SINCH_NUMBER || !process.env.SINCH_API_BASE_URL) {
console.error("FATAL: One or more required Sinch environment variables are missing. Application might not function correctly.");
}
});
// Optional: Graceful shutdown handling
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
// Add cleanup logic here (e.g., close database connections)
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
});
Explanation:
app.js:
Initializes the Express application (app).
Loads dotenv early.
Includes essential middleware: a basic request logger (recommend replacing with pino-http or similar for production), express.json() for parsing JSON bodies, and express.urlencoded() (optional).
Includes placeholders for security middleware (rate limiting, etc. - see Section 7).
Mounts the mmsRoutes under the /api/mms path prefix.
Adds a basic /health check endpoint for monitoring.
Includes a 404 handler for requests that don't match any route.
Implements a global error handler middleware (must have 4 arguments: err, req, res, next) to catch unhandled errors from routes or next(error) calls. It logs the error and sends a standardized JSON error response to the client, avoiding leaking stack traces in production.
server.js:
Imports the configured app from app.js.
Loads dotenv again (safe if already loaded) to ensure environment variables are available here too.
Gets the PORT and NODE_ENV from environment variables (with defaults).
Starts the Express server using app.listen and stores the server instance.
Logs essential configuration status on startup for quick verification, including warnings if critical Sinch variables are missing.
Includes optional basic graceful shutdown handling for SIGTERM, using the stored server instance.
How to Implement Error Handling, Logging, and Retry Logic for MMS
We've built foundational error handling. Let's refine logging and consider retries.
Error Handling Strategy (Recap):
sinchService.js: Catches axios errors during the API call. Logs detailed Sinch API error responses. Throws a new, informative Error object containing relevant details.
mmsController.js: Uses a try...catch block to catch errors from sinchService. Logs a summary. Responds to the client with an appropriate HTTP status (400 for known client/API errors, 500 for unexpected internal errors) and a JSON error message derived from the caught error.
app.js (Global Handler): Catches any errors missed by route handlers or thrown unexpectedly. Logs the full stack trace (server-side) and sends a generic 500 response to the client.
Logging Enhancements (Production):
Standard console.log/error is okay for development, but insufficient for production. Use a dedicated, structured logging library like pino.
Benefits: Structured JSON output (machine-readable), log levels (info, debug, warn, error, fatal), faster performance, easy integration with log shippers (Fluentd, Logstash) and management systems (ELK, Datadog, Splunk).
Implementation:
Install: npm install pino pino-http
Configure: Create a logger instance (e.g., in src/utils/logger.js).
Integrate pino-http middleware in app.js for automatic request/response logging.
Replace console.log/error calls throughout your services and controllers with logger.info(), logger.warn(), logger.error(), etc. Pass error objects directly to logger.error(error, "Optional message") for stack trace logging.
Retry Mechanisms:
Network issues or temporary Sinch API unavailability (e.g., 5xx errors) can occur. Retrying failed requests can improve resilience.
When to Retry: Typically retry on transient errors: network timeouts, DNS issues, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout. Do not retry on permanent errors like 4xx (Bad Request, Unauthorized, Not Found) as the request is unlikely to succeed without changes.
Strategy: Implement exponential backoff (wait longer between each retry) to avoid overwhelming the API.
Library: axios-retry simplifies this.
Example using axios-retry:
Install: npm install axios-retry
Modify src/services/sinchService.js:
JavaScript
// src/services/sinchService.jsconst axios = require('axios');
// Use default import for axios-retryconst axiosRetry = require('axios-retry').default;
// ... other imports and config (SINCH_SERVICE_PLAN_ID, TOKEN, BASE_URL, SEND_MMS_ENDPOINT) ...// Create an Axios instance specifically for Sinch requestsconst sinchApiClient = axios.create({
baseURL: SINCH_API_BASE_URL, // Set base URL here if consistenttimeout: 15000, // Default timeout for requestsheaders: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${SINCH_API_TOKEN}`// Set common headers
}
});
// Configure axios-retry on the instanceaxiosRetry(sinchApiClient, {
retries: 3, // Number of retriesretryDelay: (retryCount) => {
console.log(`Retry attempt: ${retryCount}`);
return retryCount * 1000; // Exponential backoff (1s, 2s, 3s)
},
retryCondition: (error) => {
// Retry on network errors or specific server errors (5xx)return (
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
(error.response && error.response.status >= 500 && error.response.status <= 599)
);
},
onRetry: (retryCount, error, requestConfig) => {
console.warn(`Retrying request to ${requestConfig.url} due to error: ${error.message}. Attempt ${retryCount}`);
}
});
// ... inside sendMms function ...try {
// Use the configured axios instance instead of axios.post directly// Note: If SEND_MMS_ENDPOINT was constructed using SINCH_API_BASE_URL,// you might need to adjust the URL passed here (e.g., just the path part).// Assuming SEND_MMS_ENDPOINT is the full URL:// const response = await sinchApiClient.post(SEND_MMS_ENDPOINT, payload);// OR if SINCH_API_BASE_URL is set in the instance and SEND_MMS_ENDPOINT// contains the full URL, extract the path:const endpointPath = newURL(SEND_MMS_ENDPOINT).pathname; // Requires full URL in SEND_MMS_ENDPOINTconst response = await sinchApiClient.post(endpointPath, payload);
// ... rest of the success/error handling logic ...
} catch (error) {
// Error handling remains largely the same, but logs might show retry attemptsconsole.error('Error sending MMS via Sinch (after potential retries):', error.message);
// ... rest of error handling ...
}
// ... rest of the file ...
(Note: The axios-retry example above is illustrative and requires careful integration with the existing sendMms function, particularly regarding how the URL/endpoint is passed to the sinchApiClient.post method based on whether baseURL is set on the instance.)
Frequently Asked Questions About Sinch MMS Integration
What Node.js version do I need for Sinch MMS integration?
You need Node.js 18 or later for this Sinch MMS implementation. As of January 2025, Node.js 22 (codenamed 'Jod') is the current LTS version recommended for production. Node.js 18.x reaches end-of-life on April 30, 2025 – plan to upgrade to Node.js 20 or 22 for continued support. This guide uses Express v5.x, which requires Node.js 18+ and includes security improvements and async error handling.
What are the MMS file size limits for Sinch API?
Sinch MMS supports up to 8 slides per message, with each slide containing text and/or one media type (image, audio, video). The recommended file size is under 500 KB for reliable delivery across all carriers. While most US and Canadian carriers accept up to 1 MB, carrier-specific limits vary: AT&T accepts up to 1 MB, Verizon up to 1.7 MB, and T-Mobile up to 3 MB. All media files must serve a valid Content-Length header and proper Content-Type headers (e.g., image/jpeg, video/mp4).
How do I authenticate with the Sinch MMS API?
Sinch MMS API uses Bearer token authentication. Include your API token in the Authorization header as Bearer YOUR_API_TOKEN. You'll also need your Service Plan ID (sometimes called Campaign ID), which may be included in the API endpoint path or request payload depending on your API version. Always verify the authentication method and endpoint structure in the official Sinch documentation for your specific account and region (US, EU, AU).
What's the difference between Sinch MMS JSON API and XML API?
Sinch offers two MMS APIs: the XML API (industry standard for sending rich multimedia content with higher throughput) and the MM7 API (SOAP-based protocol used by carriers). This guide focuses on the JSON API, which sends MMS messages defined in JSON format to mobile numbers via HTTP POST requests. The JSON API requires UTF-8 encoded JSON data and supports slides with embedded text, images, videos, audio, and other objects.
How do I handle MMS delivery failures and fallback to SMS?
The Sinch MMS API includes automatic fallback to SMS when MMS delivery fails or is disabled by the recipient. Set fallback-sms-text in your payload with the SMS message to send if MMS fails. You can disable fallback by setting disableFallbackSms: true, though this is not recommended. Set disable-fallback-sms-link: false to include a link to view media in the fallback SMS. Implement delivery status webhooks to track message delivery and handle failures in your application logic.
What error handling should I implement for Sinch MMS requests?
Implement comprehensive error handling for three scenarios: API errors (4xx/5xx status codes), network errors (timeouts, DNS failures), and request setup errors. Log detailed error information including Sinch API error responses. Use axios-retry with exponential backoff for transient errors (5xx, network issues) but don't retry 4xx client errors. Set reasonable timeouts (15 seconds recommended). Use structured logging (pino) in production. Implement global error handlers in Express to catch unhandled errors.
How do I validate phone numbers and media URLs for MMS?
Validate that phone numbers use E.164 international format (starting with +, e.g., +12223334444) for both sender and recipient. Check that the recipient number is MMS-capable. Validate media URLs are publicly accessible and return proper Content-Length and Content-Type headers. Verify file sizes are under carrier limits (500 KB recommended). Validate slide count (max 8), subject length (max 80 chars, 40 recommended), and text length (up to 1600 chars). Use express-validator middleware for robust production validation.
What's the recommended project structure for Sinch MMS integration?
Use a layered architecture: services/ for business logic (Sinch API interaction), controllers/ for request/response handling, routes/ for API route definitions, and app.js for Express configuration. Separate concerns by keeping API logic in the service layer, validation in middleware, and HTTP handling in controllers. Store credentials in .env files (never commit to version control). Use environment-specific configurations. Implement proper error boundaries at each layer.
How do I test MMS sending in development without sending real messages?
Use Sinch's sandbox/test environment if available for your account. Mock the Sinch API responses using tools like nock or msw (Mock Service Worker) in your unit tests. Create integration tests that verify request payloads without calling the real API. Use Jest for testing with axios-mock-adapter to intercept HTTP requests. Implement feature flags to disable MMS sending in development. Log request payloads to verify correct message formatting before enabling production sends.
What are the rate limits for Sinch MMS API?
Sinch MMS API rate limits vary by account and service plan. Each service plan has a maximum messages per second (MPS) setting, with default limits varying by account type. For batch messages with multiple recipients, each recipient counts toward the rate limit. Status query limits are typically 1 request per second per IP address (HTTP 429 if exceeded). Maximum API concurrency is often 700 requests per second per IP address. Check your specific rate limits in the Sinch Dashboard under your service plan details and contact Sinch support for higher volume adjustments.
Frequently asked questions
You can send MMS messages by creating a Node.js application with Express that uses the Sinch MMS JSON API. This involves setting up a project with dependencies like Axios and dotenv, configuring your Sinch credentials, and implementing an API endpoint to handle MMS requests. Refer to the provided code examples for a step-by-step guide.
The Sinch MMS JSON API is a specific Sinch API endpoint and protocol for sending multimedia messages. It's important to consult the official Sinch documentation for the most up-to-date details on its structure, authentication methods, and required parameters, as these may change.
Sinch offers multiple APIs, such as legacy APIs and a unified Messages API, to cater to different integration needs and preferences. This tutorial specifically uses the MMS JSON API for its dedicated MMS capabilities.
Use the Sinch MMS JSON API when you need to send multimedia messages (MMS) containing images, videos, or audio alongside text within your application. It's ideal for automating communication workflows requiring rich media.
While this guide utilizes Axios, you can potentially use other HTTP client libraries for Node.js, but you'll have to adapt the code to the respective client's API and usage patterns. Axios is recommended for its ease of use with promises.
Set up Sinch credentials by creating a .env file containing your SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, and SINCH_NUMBER. These are essential for interacting with the Sinch API. Never commit the .env file to version control, as it contains sensitive information.
The fallbackText parameter provides the message content for a fallback SMS if the recipient's device cannot receive MMS or if MMS delivery fails. It's required unless fallback is explicitly disabled using disableFallbackSms.
The MMS payload should be a JSON object with specific keys like 'to', 'from', 'message-subject', and 'slide'. The 'slide' key holds an array of slide objects. You must verify the correct structure according to the latest Sinch documentation for the MMS JSON API.
The provided code examples demonstrate error handling using try...catch blocks and status codes. The code logs detailed error information from Sinch, allowing you to identify the cause of failure. For production, enhance logging with tools like Pino and consider retry mechanisms with exponential backoff for transient errors.
The client-reference parameter is an optional field where you can include your own internal reference ID for the message, up to a recommended maximum of 64 characters. This helps you track and identify messages within your system.
Implement retry mechanisms when you want to increase the resilience of your MMS sending. Retries are useful for transient errors like network issues or temporary Sinch API unavailability (5xx errors). Use exponential backoff to prevent overwhelming the API.
Verifying the official Sinch MMS API documentation is crucial because endpoint URLs, payload structures, and other API specifics may vary depending on factors like your account settings, region, and API version. Using outdated information can lead to integration errors.
Media (images, videos, etc.) are added using the 'slides' parameter. Each 'slide' object can have media specified as a URL. These media URLs must be publicly accessible and return a Content-Length header. Always consult the Sinch API documentation for correct slide structure.
You'll build a production-ready Node.js application using the Express framework to send Multimedia Messaging Service (MMS) messages via the Sinch MMS JSON API. This guide covers everything from initial project setup to deployment and monitoring.
Important: Sinch offers multiple APIs (e.g., legacy APIs, unified Messages API). This guide focuses on a specific MMS JSON API structure. Always verify endpoints, payload structures, authentication methods, and specific behaviors against the current, official Sinch documentation relevant to your account and the specific API product version you are using. API details can change.
By the end of this tutorial, you will have a functional Express API endpoint capable of accepting MMS details (recipient number, media URLs, text) and dispatching the message through Sinch, including handling fallbacks to SMS.
Project Overview and Goals
Goal: To create a robust backend service that enables sending MMS messages programmatically using Sinch's dedicated MMS API.
Problem Solved: Provides a reliable way to integrate rich media messaging into applications, automating communication workflows that require images, videos, or other media alongside text. This is essential for marketing campaigns, transactional notifications with visual content, and customer engagement workflows that need multimedia support.
Technologies Used:
.envfile for secure credential management.System Architecture:
(Note: This is a simplified text-based representation.)
Prerequisites:
Content-Lengthheader.How to Set Up a Node.js Project for Sinch MMS Integration
Initialize your Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
mkdir sinch-mms-sender cd sinch-mms-senderStep 2: Initialize Node.js Project
Initialize the project using npm. Accept the defaults or customize as needed.
This creates a
package.jsonfile.Step 3: Install Dependencies
We need Express for the server, Axios to make HTTP requests, and dotenv to handle environment variables. We'll also add
nodemonas a development dependency for easier development.Step 4: Configure Environment Variables
Create a file named
.envin the root of your project. This file will store your sensitive credentials and configuration. Never commit this file to version control.SINCH_SERVICE_PLAN_ID: Found on your Sinch Customer Dashboard. The exact name and location might vary. ReplaceYOUR_SERVICE_PLAN_IDwith your actual ID.SINCH_API_TOKEN: Found on the Sinch Customer Dashboard, often near the Service Plan ID configuration. ReplaceYOUR_API_TOKENwith your actual token.SINCH_NUMBER: A virtual number assigned to your Sinch account, capable of sending MMS. Use the E.164 international format (e.g.,+12223334444). ReplaceYOUR_SINCH_PHONE_NUMBERwith your actual Sinch number.SINCH_API_BASE_URL: The base URL for the Sinch MMS API. Crucially, verify the correct URL and structure in the official Sinch documentation. It can vary based on region, API version, and whether the Service Plan ID is part of the path. The example provided is common but requires verification. ReplaceYOUR_SERVICE_PLAN_IDwithin the URL if this structure is correct for your API.PORT: The port your Express server will listen on.Step 5: Set Up Project Structure
Create the following basic structure:
Create the
srcdirectory and the subdirectories/files within it.Step 6: Create
.gitignoreCreate a
.gitignorefile in the project root to prevent committing sensitive files and unnecessary directories.Step 7: Add Run Scripts to
package.jsonModify the
scriptssection in yourpackage.json:{ "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "test": "echo \"Error: no test specified\" && exit 1" } }(Note: The
testscript is a placeholder. It will be updated in Section 13 when testing with Jest is introduced.)npm start: Runs the application using Node.npm run dev: Runs the application usingnodemon, which automatically restarts the server on file changes.How to Implement the Sinch MMS Service Layer
This service will encapsulate the logic for interacting with the Sinch MMS API.
File:
src/services/sinchService.js// src/services/sinchService.js const axios = require('axios'); // Load environment variables require('dotenv').config(); const SINCH_SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID; const SINCH_API_TOKEN = process.env.SINCH_API_TOKEN; const SINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL; // Assumes base URL includes service ID path per .env example if (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN || !SINCH_API_BASE_URL) { console.error("Error: Missing Sinch credentials or API base URL in .env file. Please check SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, and SINCH_API_BASE_URL."); process.exit(1); // Stop the application if critical config is missing } // Construct the specific endpoint for the sendmms action // **VERIFY THIS ENDPOINT STRUCTURE** against the official Sinch MMS JSON API documentation for your account/region. // This assumes the base URL already contains '/v1/services/{service_plan_id}' const SEND_MMS_ENDPOINT = `${SINCH_API_BASE_URL}/actions/sendmms`; /** * Sends an MMS message using the Sinch MMS JSON API. * * @param {string} to The recipient's phone number in E.164 international format (e.g., +1...). * @param {string} from The Sinch phone number sending the message (E.164 format). * @param {string} subject The subject line for the MMS (max 80 chars, 40 recommended). * @param {Array<object>} slides An array of slide objects. Structure MUST match Sinch API docs. * Each slide can contain text and/or one media type (image, audio, video, etc.). * @param {string} [fallbackText] Optional text for the fallback SMS if MMS fails or is disabled. Required if fallback isn't explicitly disabled. * @param {object} [options] Optional parameters like clientReference, disableFallbackSms, etc. (Refer to Sinch Docs). * @returns {Promise<object>} The success response data from the Sinch API (contains tracking-id). * @throws {Error} If the API request fails or returns an error status. */ const sendMms = async (to, from, subject, slides, fallbackText, options = {}) => { console.log(`Attempting to send MMS to ${to} from ${from}`); // Basic validation if (!to || !from || !subject || !Array.isArray(slides) || slides.length === 0) { throw new Error('Missing required parameters: to, from, subject, or non-empty slides array.'); } if (!to.startsWith('+') || !from.startsWith('+')) { console.warn(`Warning: Phone numbers should be in E.164 format (start with +). To: ${to}, From: ${from}`); // Consider throwing an error here in production for strict validation } if (slides.length > 8) { console.warn('Warning: Maximum of 8 slides recommended per Sinch guidelines.'); } // Ensure fallbackText is provided if fallback SMS is enabled (default behavior) const disableFallbackSms = options.disableFallbackSms === true; if (!disableFallbackSms && !fallbackText) { // Fallback is enabled by default, so text is required unless explicitly disabled. throw new Error('fallbackText is required when disableFallbackSms is false (or not provided).'); } // **VERIFY PAYLOAD STRUCTURE** against official Sinch MMS JSON API documentation. const payload = { action: 'sendmms', 'service-id': SINCH_SERVICE_PLAN_ID, // May or may not be required depending on API structure/endpoint used to: to, from: from, 'message-subject': subject, slide: slides, // Array of slide objects - structure is critical // --- Optional Parameters (Verify names and values in Sinch Docs) --- 'fallback-sms-text': disableFallbackSms ? undefined : fallbackText, // Only include if fallback is enabled 'disable-fallback-sms': options.disableFallbackSms || false, // Default: false (fallback enabled) 'disable-fallback-sms-link': options.disableFallbackSmsLink || false, // Default: false // 'fallback-sms-link-expiration': options.fallbackLinkExpiration, // Example: ISO8601 Date String // 'mms-expiry-timestamp': options.mmsExpiryTimestamp, // Example: ISO8601 Date String 'client-reference': options.clientReference, // Your internal reference ID (max 64 chars recommended) 'cache-content': options.cacheContent === undefined ? true : options.cacheContent, // Default: true (Sinch may cache media) }; // Remove undefined optional keys to keep payload clean Object.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]); console.log('Constructed Sinch Payload:', JSON.stringify(payload, null, 2)); // Log payload for debugging try { // **VERIFY AUTHENTICATION METHOD** (Bearer Token is common, but check Sinch Docs) const response = await axios.post(SEND_MMS_ENDPOINT, payload, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Authorization': `Bearer ${SINCH_API_TOKEN}` // Standard Bearer token auth }, timeout: 15000 // Set a reasonable timeout (e.g., 15 seconds) for the API call }); console.log('Sinch API Response Status:', response.status); console.log('Sinch API Response Data:', JSON.stringify(response.data, null, 2)); // Sinch often returns 200 OK even for queued messages, but check response body for success indicator if available // The exact success condition might vary based on the API version. This checks for a common pattern. if (response.data && response.data.status === 'success' && response.data['tracking-id']) { console.log(`MMS successfully queued with Sinch. Tracking ID: ${response.data['tracking-id']}`); return response.data; // Contains tracking-id etc. } else { // Handle cases where Sinch might return 200 but indicate failure/issue in the body, or success structure differs console.error('Sinch API response body indicates potential issue:', response.data); throw new Error(`Sinch API did not return expected success status or tracking-id in response body: ${JSON.stringify(response.data)}`); } } catch (error) { console.error('Error sending MMS via Sinch:', error.message); let errorMessage = 'Failed to send MMS via Sinch API.'; if (error.response) { // The request was made and the server responded with a status code outside the 2xx range console.error('Sinch API Error Status:', error.response.status); console.error('Sinch API Error Headers:', error.response.headers); console.error('Sinch API Error Data:', JSON.stringify(error.response.data, null, 2)); // Include details from Sinch error response if available const errorDetails = error.response.data ? `: ${JSON.stringify(error.response.data)}` : ''; errorMessage = `Sinch API request failed with status ${error.response.status}${errorDetails}`; } else if (error.request) { // The request was made but no response was received console.error('Sinch API No Response Received. Request details:', error.request); errorMessage = 'No response received from Sinch API. Check network connectivity and endpoint URL.'; } else { // Something happened in setting up the request that triggered an Error console.error('Axios Request Setup Error:', error.message); errorMessage = `Error setting up request to Sinch API: ${error.message}`; } // Re-throw a more informative error to be handled by the controller throw new Error(errorMessage); } }; module.exports = { sendMms, };Explanation:
axiosanddotenv. Loads environment variables and checks for essential Sinch credentials and the base URL, exiting if missing.sendmmsaction based on theSINCH_API_BASE_URL. Includes a prominent warning to verify this endpoint against official documentation.sendMmsFunction:to,from,subject,slides) and optionalfallbackTextandoptions.fallbackTextis present if fallback SMS is not explicitly disabled.payloadaccording to a common Sinch MMS JSON API specification. Includes a warning to verify the payload structure against official documentation. Handles common optional fields and cleans undefined values.axios.postto send the request.Content-TypeandAuthorization(Bearer token - verify this method).status: 'success'andtracking-id) as 200 OK might not guarantee acceptance. This check might need adjustment based on the specific API response structure.How to Build Express API Routes and Controllers for MMS
Now, let's create the Express route and controller to expose our MMS sending functionality.
File:
src/routes/mmsRoutes.js// src/routes/mmsRoutes.js const express = require('express'); const mmsController = require('../controllers/mmsController'); // Optional: Import validation middleware if using express-validator (see Section 7) // const { validateSendMms } = require('../middleware/validators'); // Example path const router = express.Router(); // Define the route for sending MMS // POST /api/mms/send // Optional: Add validation middleware before the controller // router.post('/send', validateSendMms, mmsController.handleSendMms); router.post('/send', mmsController.handleSendMms); module.exports = router;File:
src/controllers/mmsController.js// src/controllers/mmsController.js const sinchService = require('../services/sinchService'); // Load environment variables to get the sender number require('dotenv').config(); const SINCH_NUMBER = process.env.SINCH_NUMBER; if (!SINCH_NUMBER) { console.error("FATAL: SINCH_NUMBER is not defined in .env file. Cannot determine sender number."); process.exit(1); } /** * Handles the incoming API request to send an MMS. * Expects request body adhering to the defined contract, e.g.: * { * "to": "+1xxxxxxxxxx", // E.164 format * "subject": "Your Media Update", // Max 80 chars * "slides": [ // 1-8 slides, structure defined by Sinch API docs * { "image": { "url": "https://your-cdn.com/image.jpg" }, "message-text": "Check out this picture!" }, * { "video": { "url": "https://your-cdn.com/video.mp4" }, "message-text": "And this video too." } * ], * "fallbackText": "View your media update here: [link inserted by Sinch if enabled]", // Required if fallback not disabled * "clientReference": "internal-order-123", // Optional, max 64 chars * "disableFallbackSms": false, // Optional, default false * "disableFallbackSmsLink": false // Optional, default false * } */ const handleSendMms = async (req, res) => { // Extract data from request body const { to, subject, slides, fallbackText, clientReference, disableFallbackSms, disableFallbackSmsLink /* Add other options if needed */ } = req.body; const from = SINCH_NUMBER; // Use the number configured in .env as the sender // Basic Input Validation (Strongly recommend using a library like express-validator for production - see Section 7) if (!to || !subject || !Array.isArray(slides) || slides.length === 0) { console.warn(`Validation failed for incoming request: Missing required fields.`); return res.status(400).json({ status: 'error', message: 'Bad Request: Missing required fields: `to`, `subject`, `slides` (must be a non-empty array).', }); } // Add more specific validation here or use middleware (e.g., check 'to' format, subject length, slide content structure) // Prepare options object for the service layer const serviceOptions = { clientReference, disableFallbackSms, disableFallbackSmsLink, // Pass through other relevant options extracted from req.body }; try { console.log(`Processing request to send MMS via Sinch: To=${to}, From=${from}, Subject=${subject}`); const result = await sinchService.sendMms( to, from, subject, slides, fallbackText, // Pass fallbackText explicitly serviceOptions // Pass the options bundle ); // If sendMms resolves, it means the request was accepted by Sinch (or seemed to be) console.log(`Successfully queued MMS to ${to} via Sinch. Tracking ID: ${result['tracking-id']}`); // Respond with the success status and the data received from Sinch (including tracking ID) res.status(200).json({ status: 'success', message: 'MMS request accepted by Sinch and queued for delivery.', data: result, // Send back the full Sinch success response }); } catch (error) { // Log the detailed error caught from the service layer console.error(`Failed to process MMS request for ${to}:`, error.message); // error.message now contains detailed info // Determine appropriate status code based on error type if possible // If the error came from Sinch API (indicated by the service layer error message), use 400 or a more specific code if known. // Otherwise, use 500 for internal server errors. const statusCode = error.message.includes("Sinch API request failed") || error.message.includes("required parameters") || error.message.includes("fallbackText is required") ? 400 : 500; res.status(statusCode).json({ status: 'error', message: 'Failed to send MMS.', // Provide the specific error message back (be cautious about leaking too much detail in production) error: error.message, }); } }; module.exports = { handleSendMms, };Explanation:
mmsRoutes.js): Defines a single POST route/api/mms/sendthat maps to thehandleSendMmscontroller function. Includes a commented-out example showing where validation middleware (Section 7) would go.mmsController.js):sinchService.SINCH_NUMBERfrom environment variables to use as thefromnumber, exiting if not found.to,subject,slides,fallbackText, and optional fields likeclientReference,disableFallbackSms) from the request body (req.body).express-validator(Section 7) for robust validation in production.serviceOptionsobject.sinchService.sendMmswith the extracted and validated data.tracking-id).How to Configure Express Application with Sinch MMS Integration
Let's tie everything together in our main Express application file.
File:
src/app.js// src/app.js const express = require('express'); const mmsRoutes = require('./routes/mmsRoutes'); // Optional: Import security middleware like rate limiter (see Section 7) // const { apiLimiter } = require('./middleware/rateLimiter'); // Optional: Import request logger like pino-http (see Section 5) // const { httpLogger } = require('./utils/logger'); // Load environment variables early, especially for PORT and NODE_ENV require('dotenv').config(); const app = express(); // --- Core Middlewares --- // Request Logging (Replace with a structured logger like pino in production - See Section 5) // app.use(httpLogger); // Example using pino-http app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] Incoming Request: ${req.method} ${req.originalUrl}`); next(); // Pass control to the next middleware/route handler }); // JSON Body Parser: Crucial for reading req.body from JSON payloads app.use(express.json({ limit: '1mb' })); // Set a reasonable payload size limit // URL-encoded Body Parser (optional, if you expect form data) app.use(express.urlencoded({ extended: true })); // --- Security Middlewares (Examples - See Section 7) --- // app.use('/api/', apiLimiter); // Apply rate limiting to API routes // Add other security middleware here (helmet, cors if needed) // --- API Routes --- app.use('/api/mms', mmsRoutes); // Mount the MMS routes under /api/mms // --- Health Check Endpoint --- app.get('/health', (req, res) => { // Basic health check, can be expanded later res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // --- Error Handling Middlewares (Must be defined LAST) --- // Catch-all for 404 Not Found errors (requests falling through routes) app.use((req, res, next) => { res.status(404).json({ status: 'error', message: `Resource not found: ${req.originalUrl}` }); }); // Global Error Handler: Catches errors passed via next(error) or thrown in async route handlers // Needs 4 arguments to be recognized by Express as an error handler app.use((err, req, res, next) => { // Log the error internally (use a proper logger in production) console.error("Unhandled Error:", err.stack || err); // Determine status code: Use error's status if set, otherwise default to 500 const statusCode = typeof err.status === 'number' ? err.status : 500; // Send generic error response to the client res.status(statusCode).json({ status: 'error', message: err.message || 'Internal Server Error', // Optionally include stack trace in development mode ONLY ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); module.exports = app;File:
src/server.js// src/server.js const app = require('./app'); // Ensure environment variables are loaded (dotenv.config() might be called in app.js already) require('dotenv').config(); const PORT = process.env.PORT || 3000; // Use PORT from .env or default to 3000 const NODE_ENV = process.env.NODE_ENV || 'development'; const server = app.listen(PORT, () => { // Store server instance for graceful shutdown console.log(`Server running in ${NODE_ENV} mode on port ${PORT}`); console.log("--- Configuration Check ---"); console.log(`Sinch Service Plan ID configured: ${process.env.SINCH_SERVICE_PLAN_ID ? 'Yes' : 'NO - Check .env!'}`); console.log(`Sinch API Token configured: ${process.env.SINCH_API_TOKEN ? 'Yes' : 'NO - Check .env!'}`); console.log(`Sinch Sender Number configured: ${process.env.SINCH_NUMBER || 'NO - Check .env!'}`); console.log(`Sinch API Base URL configured: ${process.env.SINCH_API_BASE_URL || 'NOT SET - Check .env!'}`); console.log("---------------------------"); if (!process.env.SINCH_SERVICE_PLAN_ID || !process.env.SINCH_API_TOKEN || !process.env.SINCH_NUMBER || !process.env.SINCH_API_BASE_URL) { console.error("FATAL: One or more required Sinch environment variables are missing. Application might not function correctly."); } }); // Optional: Graceful shutdown handling process.on('SIGTERM', () => { console.log('SIGTERM signal received: closing HTTP server'); // Add cleanup logic here (e.g., close database connections) server.close(() => { console.log('HTTP server closed'); process.exit(0); }); });Explanation:
app.js:app).dotenvearly.pino-httpor similar for production),express.json()for parsing JSON bodies, andexpress.urlencoded()(optional).mmsRoutesunder the/api/mmspath prefix./healthcheck endpoint for monitoring.err, req, res, next) to catch unhandled errors from routes ornext(error)calls. It logs the error and sends a standardized JSON error response to the client, avoiding leaking stack traces in production.server.js:appfromapp.js.dotenvagain (safe if already loaded) to ensure environment variables are available here too.PORTandNODE_ENVfrom environment variables (with defaults).app.listenand stores the server instance.SIGTERM, using the storedserverinstance.How to Implement Error Handling, Logging, and Retry Logic for MMS
We've built foundational error handling. Let's refine logging and consider retries.
Error Handling Strategy (Recap):
sinchService.js: Catchesaxioserrors during the API call. Logs detailed Sinch API error responses. Throws a new, informativeErrorobject containing relevant details.mmsController.js: Uses atry...catchblock to catch errors fromsinchService. Logs a summary. Responds to the client with an appropriate HTTP status (400 for known client/API errors, 500 for unexpected internal errors) and a JSON error message derived from the caught error.app.js(Global Handler): Catches any errors missed by route handlers or thrown unexpectedly. Logs the full stack trace (server-side) and sends a generic 500 response to the client.Logging Enhancements (Production):
Standard
console.log/erroris okay for development, but insufficient for production. Use a dedicated, structured logging library likepino.npm install pino pino-httpsrc/utils/logger.js).pino-httpmiddleware inapp.jsfor automatic request/response logging.console.log/errorcalls throughout your services and controllers withlogger.info(),logger.warn(),logger.error(), etc. Pass error objects directly tologger.error(error, "Optional message")for stack trace logging.Retry Mechanisms:
Network issues or temporary Sinch API unavailability (e.g., 5xx errors) can occur. Retrying failed requests can improve resilience.
axios-retrysimplifies this.Example using
axios-retry:npm install axios-retrysrc/services/sinchService.js:// src/services/sinchService.js const axios = require('axios'); // Use default import for axios-retry const axiosRetry = require('axios-retry').default; // ... other imports and config (SINCH_SERVICE_PLAN_ID, TOKEN, BASE_URL, SEND_MMS_ENDPOINT) ... // Create an Axios instance specifically for Sinch requests const sinchApiClient = axios.create({ baseURL: SINCH_API_BASE_URL, // Set base URL here if consistent timeout: 15000, // Default timeout for requests headers: { 'Content-Type': 'application/json; charset=utf-8', 'Authorization': `Bearer ${SINCH_API_TOKEN}` // Set common headers } }); // Configure axios-retry on the instance axiosRetry(sinchApiClient, { retries: 3, // Number of retries retryDelay: (retryCount) => { console.log(`Retry attempt: ${retryCount}`); return retryCount * 1000; // Exponential backoff (1s, 2s, 3s) }, retryCondition: (error) => { // Retry on network errors or specific server errors (5xx) return ( axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response && error.response.status >= 500 && error.response.status <= 599) ); }, onRetry: (retryCount, error, requestConfig) => { console.warn(`Retrying request to ${requestConfig.url} due to error: ${error.message}. Attempt ${retryCount}`); } }); // ... inside sendMms function ... try { // Use the configured axios instance instead of axios.post directly // Note: If SEND_MMS_ENDPOINT was constructed using SINCH_API_BASE_URL, // you might need to adjust the URL passed here (e.g., just the path part). // Assuming SEND_MMS_ENDPOINT is the full URL: // const response = await sinchApiClient.post(SEND_MMS_ENDPOINT, payload); // OR if SINCH_API_BASE_URL is set in the instance and SEND_MMS_ENDPOINT // contains the full URL, extract the path: const endpointPath = new URL(SEND_MMS_ENDPOINT).pathname; // Requires full URL in SEND_MMS_ENDPOINT const response = await sinchApiClient.post(endpointPath, payload); // ... rest of the success/error handling logic ... } catch (error) { // Error handling remains largely the same, but logs might show retry attempts console.error('Error sending MMS via Sinch (after potential retries):', error.message); // ... rest of error handling ... } // ... rest of the file ...(Note: The
axios-retryexample above is illustrative and requires careful integration with the existingsendMmsfunction, particularly regarding how the URL/endpoint is passed to thesinchApiClient.postmethod based on whetherbaseURLis set on the instance.)Frequently Asked Questions About Sinch MMS Integration
What Node.js version do I need for Sinch MMS integration?
You need Node.js 18 or later for this Sinch MMS implementation. As of January 2025, Node.js 22 (codenamed 'Jod') is the current LTS version recommended for production. Node.js 18.x reaches end-of-life on April 30, 2025 – plan to upgrade to Node.js 20 or 22 for continued support. This guide uses Express v5.x, which requires Node.js 18+ and includes security improvements and async error handling.
What are the MMS file size limits for Sinch API?
Sinch MMS supports up to 8 slides per message, with each slide containing text and/or one media type (image, audio, video). The recommended file size is under 500 KB for reliable delivery across all carriers. While most US and Canadian carriers accept up to 1 MB, carrier-specific limits vary: AT&T accepts up to 1 MB, Verizon up to 1.7 MB, and T-Mobile up to 3 MB. All media files must serve a valid
Content-Lengthheader and properContent-Typeheaders (e.g.,image/jpeg,video/mp4).How do I authenticate with the Sinch MMS API?
Sinch MMS API uses Bearer token authentication. Include your API token in the
Authorizationheader asBearer YOUR_API_TOKEN. You'll also need your Service Plan ID (sometimes called Campaign ID), which may be included in the API endpoint path or request payload depending on your API version. Always verify the authentication method and endpoint structure in the official Sinch documentation for your specific account and region (US, EU, AU).What's the difference between Sinch MMS JSON API and XML API?
Sinch offers two MMS APIs: the XML API (industry standard for sending rich multimedia content with higher throughput) and the MM7 API (SOAP-based protocol used by carriers). This guide focuses on the JSON API, which sends MMS messages defined in JSON format to mobile numbers via HTTP POST requests. The JSON API requires UTF-8 encoded JSON data and supports slides with embedded text, images, videos, audio, and other objects.
How do I handle MMS delivery failures and fallback to SMS?
The Sinch MMS API includes automatic fallback to SMS when MMS delivery fails or is disabled by the recipient. Set
fallback-sms-textin your payload with the SMS message to send if MMS fails. You can disable fallback by settingdisableFallbackSms: true, though this is not recommended. Setdisable-fallback-sms-link: falseto include a link to view media in the fallback SMS. Implement delivery status webhooks to track message delivery and handle failures in your application logic.What error handling should I implement for Sinch MMS requests?
Implement comprehensive error handling for three scenarios: API errors (4xx/5xx status codes), network errors (timeouts, DNS failures), and request setup errors. Log detailed error information including Sinch API error responses. Use
axios-retrywith exponential backoff for transient errors (5xx, network issues) but don't retry 4xx client errors. Set reasonable timeouts (15 seconds recommended). Use structured logging (pino) in production. Implement global error handlers in Express to catch unhandled errors.How do I validate phone numbers and media URLs for MMS?
Validate that phone numbers use E.164 international format (starting with
+, e.g.,+12223334444) for both sender and recipient. Check that the recipient number is MMS-capable. Validate media URLs are publicly accessible and return properContent-LengthandContent-Typeheaders. Verify file sizes are under carrier limits (500 KB recommended). Validate slide count (max 8), subject length (max 80 chars, 40 recommended), and text length (up to 1600 chars). Useexpress-validatormiddleware for robust production validation.What's the recommended project structure for Sinch MMS integration?
Use a layered architecture:
services/for business logic (Sinch API interaction),controllers/for request/response handling,routes/for API route definitions, andapp.jsfor Express configuration. Separate concerns by keeping API logic in the service layer, validation in middleware, and HTTP handling in controllers. Store credentials in.envfiles (never commit to version control). Use environment-specific configurations. Implement proper error boundaries at each layer.How do I test MMS sending in development without sending real messages?
Use Sinch's sandbox/test environment if available for your account. Mock the Sinch API responses using tools like
nockormsw(Mock Service Worker) in your unit tests. Create integration tests that verify request payloads without calling the real API. Use Jest for testing withaxios-mock-adapterto intercept HTTP requests. Implement feature flags to disable MMS sending in development. Log request payloads to verify correct message formatting before enabling production sends.What are the rate limits for Sinch MMS API?
Sinch MMS API rate limits vary by account and service plan. Each service plan has a maximum messages per second (MPS) setting, with default limits varying by account type. For batch messages with multiple recipients, each recipient counts toward the rate limit. Status query limits are typically 1 request per second per IP address (HTTP 429 if exceeded). Maximum API concurrency is often 700 requests per second per IP address. Check your specific rate limits in the Sinch Dashboard under your service plan details and contact Sinch support for higher volume adjustments.