How to Send MMS Messages with Sinch API in Next.js and Node.js (2025)
Complete tutorial: Send MMS multimedia messages with images and videos using Sinch XMS API, Next.js, and Node.js. Includes working code, error handling, deployment, and US carrier compliance.
Learn how to send MMS multimedia messages with images and videos using Sinch XMS API, Next.js, and Node.js. This complete tutorial shows you how to build a secure Next.js API route that handles MMS delivery, create a React form for message composition, and deploy a production-ready messaging solution.
You'll build a web application with a user-friendly interface to capture recipient phone numbers, message content, and media URLs. The React frontend communicates with a Next.js API route that integrates with the Sinch XMS API to send MMS messages reliably.
Project Goals:
Set up a Next.js project configured for sending MMS messages.
Create a secure API endpoint (Next.js API Route) to handle MMS sending logic.
Integrate with the Sinch XMS API using REST principles.
Implement error handling and secure credential management.
Provide a frontend interface to trigger the MMS sending process.
Technology Stack:
Next.js: React framework providing structure, server-side rendering, and API routes (your Node.js backend environment).
Node.js: Runtime environment for your Next.js API route.
Sinch XMS API: Third-party service for dispatching MMS messages via its RESTful /batches endpoint.
axios (optional): Promise-based HTTP client for Sinch API requests. Alternatively, use the built-in fetch API.
Node.js: Version 22 LTS (Active LTS until October 2027) or later. Node.js 20 LTS is also supported.
npm or yarn: Package manager for Node.js.
Sinch Account: Registered Sinch account with access to Messaging APIs.
Sinch API Credentials: Service Plan ID and API Token from the Sinch dashboard.
Sinch Phone Number: MMS-capable phone number purchased or assigned within your Sinch account. Note: MMS functionality is only available in the US region and must be enabled by contacting your Sinch account manager.1
Publicly Accessible Media URL: URL pointing to the image or video file you want to send. Sinch must be able to fetch this URL without authentication. Supported formats include JPEG, PNG, GIF, and MP4 (check Sinch documentation for current limits). Typical size limits range from 500 KB to 5 MB depending on carrier support.
Expected Outcome:
By the end of this guide, you'll have a functional Next.js application with a form for MMS submission. Entering a recipient number, message, and media URL triggers an API call to your backend, which uses the Sinch XMS API to send the MMS message. You'll also implement error handling and user feedback.
1. Next.js Project Setup for Sinch MMS Integration
Create a new Next.js application and configure the dependencies needed for MMS messaging with Sinch.
1.1 Create the Next.js App:
Open your terminal and run the following command, replacing sinch-mms-nextjs with your desired project name:
Bash
npx create-next-app@latest sinch-mms-nextjs
Follow the prompts. Select these options:
TypeScript: No (for simplicity in this guide, but feel free to use it)
ESLint: Yes
Tailwind CSS: No (optional, not needed for this core functionality)
src/ directory: No (we'll use the default pages structure)
App Router: No (we'll use the traditional pages directory and API routes for clarity)
Import alias: Default is fine
1.2 Navigate into the Project Directory:
Bash
cd sinch-mms-nextjs
1.3 Install Dependencies:
Install axios to make HTTP requests from your backend API route to Sinch. Skip this step if you prefer the built-in fetch API.
Bash
npm install axios
1.4 Configure Environment Variables:
Never hardcode API keys. Use environment variables instead. Create a .env.local file in your project root:
Bash
touch .env.local
Open .env.local and add the following lines, replacing the placeholder values with your actual Sinch credentials and number:
Text
# .env.local
# Obtain from Sinch Dashboard: SMS → APIs → REST configuration (or similar path)
SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
SINCH_API_TOKEN=YOUR_API_TOKEN
# Obtain from Sinch Dashboard: Numbers → Your Active Numbers (Must be MMS capable)
# Use E.164 format (e.g., +12125551234)
SINCH_NUMBER=+12125551234
# Sinch API Region Base URL (Check Sinch docs for your region if not US)
# Common Regions:
# US: https://us.sms.api.sinch.com
# EU: https://eu.sms.api.sinch.com
# More available: https://developers.sinch.com/docs/sms/api-rest/ (or XMS docs)
SINCH_API_BASE_URL=https://us.sms.api.sinch.com
Explanation of Environment Variables:
SINCH_SERVICE_PLAN_ID: Your unique identifier for the API plan. Find this in the Sinch Customer Dashboard.
SINCH_API_TOKEN: Secret token to authenticate your API requests. Find this in the Sinch Customer Dashboard. Treat this like a password.
SINCH_NUMBER: MMS-capable phone number from your Sinch account used as the sender ("From" number). Must be in E.164 format (e.g., +12125551234).
SINCH_API_BASE_URL: Base URL for the Sinch Messaging API specific to your region. Ensure this matches your account setup.
Important: Add .env.local to your .gitignore file to prevent accidentally committing your credentials. create-next-app usually does this automatically.
Text
# .gitignore (ensure this line exists)
.env*.local
This structure separates concerns: pages/api/ contains server-side API routes (your Node.js backend), while pages/ contains client-side React components. Next.js automatically routes requests to /api/send-mms to the send-mms.js handler.
2. Building the Sinch MMS API Route in Next.js
Create the backend logic within a Next.js API route. This route receives requests from your frontend, constructs the MMS payload for Sinch XMS API, and makes the API call.
2.1 Create the API Route File:
Create a new file: pages/api/send-mms.js
2.2 Implement the API Handler:
Paste the following code into pages/api/send-mms.js:
JavaScript
// pages/api/send-mms.jsimport axios from'axios'; // Or use built-in fetch// Ensure environment variables are loaded (Next.js handles this automatically)constSERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID;
constAPI_TOKEN = process.env.SINCH_API_TOKEN;
constSINCH_NUMBER = process.env.SINCH_NUMBER;
constSINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL;
exportdefaultasyncfunctionhandler(req, res) {
// 1. Only allow POST requestsif (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
}
// 2. Basic Input Validationconst { to, body, mediaUrl } = req.body;
if (!to || !body || !mediaUrl) {
return res.status(400).json({ error: 'Missing required fields: to, body, mediaUrl' });
}
// Validate phone number format (basic E.164 check)const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(to)) {
return res.status(400).json({ error: 'Invalid "to" phone number format. Use E.164 (e.g., +12125551234).' });
}
// Validate media URL format (basic check)try {
newURL(mediaUrl);
} catch (error) {
return res.status(400).json({ error: 'Invalid "mediaUrl" format.' });
}
// Optional: Validate media file extensionconst validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov'];
const urlPath = newURL(mediaUrl).pathname.toLowerCase();
const hasValidExtension = validExtensions.some(ext => urlPath.endsWith(ext));
if (!hasValidExtension) {
return res.status(400).json({ error: 'Invalid media file type. Supported: JPEG, PNG, GIF, MP4, MOV.' });
}
// 3. Construct the Sinch XMS API Payload for MMS Batch// The /batches endpoint is used for sending SMS/MMS.const payload = {
from: SINCH_NUMBER,
to: [to], // API expects an array of recipientsbody: body,
parameters: { // Parameters specific to MMS often go heremedia_body: {
url: mediaUrl,
// Optional: message if URL cannot be fetched, filename// message: "Could not display media content.",// filename: "image.jpg"
}
},
// Optional: type: 'mt_media' // Explicitly setting type might be required by some Sinch setups// Optional: delivery_report: 'full' // Request delivery reports
};
// Optional parameters explained:// - delivery_report: 'none', 'summary', or 'full' – controls delivery status callbacks// - type: 'mt_media' – explicitly marks message as MMS (usually auto-detected)// - callback_url: URL where Sinch sends delivery reports (requires webhook endpoint)// To receive delivery reports, set delivery_report: 'full' and implement a webhook endpoint// 4. Prepare API Request Options// Using the XMS (Cross-channel Messaging) API endpoint for batchesconstAPI_ENDPOINT = `${SINCH_API_BASE_URL}/xms/v1/${SERVICE_PLAN_ID}/batches`;
const config = {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
};
// 5. Send the request to Sinchtry {
console.log(`Sending MMS batch request to ${API_ENDPOINT}`);
// WARNING: Do NOT log the full payload in production – it contains sensitive recipient numbers and message content.// Log only metadata or sanitized data.// console.log('Payload (DEBUG):', JSON.stringify(payload, null, 2));const sinchResponse = await axios.post(API_ENDPOINT, payload, config);
console.log('Sinch API Response Status:', sinchResponse.status);
// console.log('Sinch API Response Data:', sinchResponse.data); // Avoid logging full response data in production// 6. Handle Sinch Response (Success)// Sinch typically returns 201 Created for successful batch submission.if (sinchResponse.status === 201) {
return res.status(200).json({ // Return 200 OK to the clientsuccess: true,
message: 'MMS submitted successfully to Sinch.',
batchId: sinchResponse.data.id, // Include batch ID for tracking
});
} else {
// Handle unexpected success status codes (201 is standard)console.warn('Received unexpected success status from Sinch:', sinchResponse.status);
return res.status(500).json({ error: 'Received unexpected success status from Sinch API.', details: sinchResponse.data });
}
} catch (error) {
// 7. Handle Errorsconsole.error('Error sending MMS via Sinch:', error);
if (error.response) {
// The request was made and the server responded with a status code// that falls out of the range of 2xxconsole.error('Sinch API Error Status:', error.response.status);
console.error('Sinch API Error Data:', error.response.data);
// Forward Sinch's error status and data if availablereturn res.status(error.response.status || 500).json({
error: 'Failed to send MMS via Sinch.',
details: error.response.data || 'No details provided.',
});
} elseif (error.request) {
// The request was made but no response was receivedconsole.error('Sinch API No Response:', error.request);
return res.status(504).json({ error: 'No response received from Sinch API.' });
} else {
// Something happened in setting up the request that triggered an Errorconsole.error('Sinch API Request Setup Error:', error.message);
return res.status(500).json({ error: 'Error setting up request to Sinch API.', details: error.message });
}
}
}
Code Explanation:
Method Check: Ensures only POST requests are accepted.
Input Validation: Checks for required fields (to, body, mediaUrl) and validates the phone number (E.164), media URL format, and file extension.
Payload Construction: Creates the JSON payload for the Sinch /xms/v1/.../batches endpoint.
from: Your Sinch number.
to: An array containing the recipient's E.164 number.
body: The text part of the message.
parameters.media_body.url: Specifies the media URL for MMS via the XMS API. This URL must be publicly accessible.
Optional parameters can be added per Sinch XMS API documentation.
API Request Setup: Defines the target Sinch XMS API endpoint and sets Authorization and Content-Type headers.
API Call: Uses axios.post (or fetch) to send the request. Includes basic logging (with a warning about logging sensitive data in production).
Success Handling: Checks for the expected 201 Created status from Sinch. Returns a 200 OK response to the frontend with success status and the Sinch batchId.
Error Handling: Catches errors from the API call, logging details server-side and returning appropriate error statuses and messages to the frontend.
Common Sinch Error Responses:
Status Code
Error Type
Cause
Solution
401
unauthorized
Invalid API token or Service Plan ID
Verify credentials in .env.local
400
syntax_invalid_parameter_format
Invalid phone number format
Ensure E.164 format (e.g., +12125551234)
403
forbidden
MMS not enabled or unavailable for region
Contact Sinch account manager to enable MMS
400
syntax_constraint_violation
Media URL not accessible
Verify URL is publicly accessible without authentication
429
too_many_requests
Rate limit exceeded
Implement exponential backoff and retry logic
3. Creating the React Frontend for MMS Composition
Build a React component on a Next.js page to collect user input (recipient, message, media URL) and trigger your MMS API route.
3.1 Modify the Index Page:
Replace the contents of pages/index.js with the following code:
State Management: Uses useState for form inputs (to, body, mediaUrl), loading state (isLoading), and feedback (statusMessage).
Form Structure: Standard HTML form with inputs for recipient, message, and media URL. Basic inline styles applied.
handleSubmit Function:
Prevents default submission.
Sets loading state.
Performs basic frontend validation.
Uses fetch to POST data to /api/send-mms.
Handles the JSON response from the API route, updating statusMessage for success (showing Batch ID) or failure (showing error details from the API).
Catches network errors during fetch.
Resets loading state in the finally block.
Feedback: Displays statusMessage below the form, styled for success/error. Disables the button during loading.
Head Component: Includes meta and link tags for proper SEO.
4. Integrating with Sinch (Credentials & Dashboard)
This section reiterates how to obtain and securely manage your Sinch API credentials, which we've already placed in .env.local.
4.1 Obtaining Credentials:
Log in to your Sinch Customer Dashboard.
Navigate to SMS or Messaging APIs.
Find the REST API configuration area.
Locate your Service plan ID and API token. Click "Show" or "Generate" for the token.
Copy these values carefully.
4.2 Obtaining Your Sinch Number:
In the Sinch Customer Dashboard, navigate to Numbers.
Go to Your virtual numbers.
Identify an active number with MMS capability enabled. Verify it's in E.164 format (e.g., +1xxxxxxxxxx).
Copy this number.
4.3 Secure Storage:
Place these credentials (SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_NUMBER) into your .env.local file.
Never commit .env.local or any file containing your API token to version control. Ensure .env*.local is in your .gitignore file.
When deploying, use your hosting provider's environment variable management.
Environment Variable Configuration by Platform:
Platform
Configuration Location
Documentation
Vercel
Project Settings → Environment Variables
Add variables for Production, Preview, and Development
Netlify
Site Settings → Environment Variables
Configure build and runtime variables
AWS Amplify
App Settings → Environment Variables
Set variables per branch
Railway
Project → Variables
Auto-injects into runtime
Render
Environment → Environment Variables
Supports .env file upload
5. Error Handling and Logging
We've implemented error handling in both the API route and the frontend.
API Route (pages/api/send-mms.js):
Input Validation: Returns 400 Bad Request for missing/invalid inputs.
Method Check: Returns 405 Method Not Allowed for non-POST requests.
Sinch API Errors: Catches errors from the axios.post call, logs details server-side (console.error), and returns specific status codes/messages to the frontend.
Logging: Uses console.log and console.error. Important: In a production environment, replace console.log/console.error with a robust logging library (e.g., Pino, Winston). Critically, ensure that any debug logging (like the commented-out payload log in the API route) is removed or heavily sanitized in production to avoid logging sensitive information like recipient phone numbers or message content.
Frontend (pages/index.js):
API Response Errors: Displays error messages received from the API route.
Fetch Errors: Catches network errors during the fetch call.
Loading State: Provides visual feedback during the API call.
Retry Mechanisms (Advanced):
For production, consider implementing retries with exponential backoff in the API route for transient network errors or temporary Sinch API issues (e.g., 5xx errors). Libraries like axios-retry can help with this implementation.
6. Database Schema and Data Layer (Not Applicable)
This guide focuses on the stateless action of sending an MMS and does not require a database. For applications needing message history or tracking, integrate a database (e.g., PostgreSQL, MongoDB) and data layer (e.g., Prisma, TypeORM).
7. Adding Security Features
Environment Variables: (Covered) Keep secrets out of code.
Input Validation: (Covered) Essential on both client and server (API route). Sanitize inputs if displayed elsewhere.
HTTPS: Use HTTPS for your Next.js app in production. Sinch API communication is already over HTTPS.
Rate Limiting: (Advanced) Implement rate limiting on /api/send-mms to prevent abuse. Use libraries like rate-limiter-flexible or platform features.
Authentication/Authorization: (Advanced) Protect the API route using authentication (e.g., NextAuth.js, Clerk) to ensure only authorized users can send MMS.
URL Validation: The mediaUrl is fetched by Sinch. While basic URL validation is in place, ensure your validation prevents users from submitting malicious or invalid URLs. Robust URL validation and file extension checking protect against abuse.
8. Handling Special Cases (MMS Specific)
E.164 Format: Strictly required by Sinch for all phone numbers.
Public Media URL: Must be publicly accessible without authentication for Sinch to fetch. The URL should return appropriate Content-Type headers.
Media File Types/Size: Check Sinch documentation for supported types (JPEG, PNG, GIF, MP4, etc.) and size limits. Common limits are around 500 KB to 5 MB depending on carrier support. The code includes basic file extension validation.
Character Limits: MMS allows more text than SMS (typically 1,600 characters), but practical limits exist based on carrier support.
Regional Restrictions:MMS is only available in the US region.1 Contact your Sinch account manager to enable MMS functionality on your account. Sending MMS to or from non-US regions will fail. For international messaging, use SMS or other supported channels.
Carrier Compliance: Adhere to content guidelines and regulations (e.g., CTIA guidelines in the US, TCPA (Telephone Consumer Protection Act) compliance for commercial messages). Ensure you have proper consent from recipients before sending commercial MMS messages. Implement opt-in/opt-out mechanisms to comply with TCPA requirements.
Performance is mainly dictated by the Sinch API response time.
API Route Performance: Keep the handler lightweight; avoid blocking operations before the API call.
Frontend Performance: Standard React optimization techniques apply if the page grows complex.
Caching: Not applicable for the sending action itself.
Resource Usage: Ensure adequate server resources for expected volume.
10. Adding Monitoring, Observability, and Analytics (Basic Concepts)
For production:
Health Checks: Implement a /api/health endpoint.
Performance Metrics: Use APM tools (Datadog, New Relic, Sentry) to monitor API route response times and error rates.
Error Tracking: Use services like Sentry or Bugsnag for capturing exceptions.
Sinch Dashboard: Monitor API logs and delivery reports in the Sinch portal.
Logging: Implement structured logging (Pino/Winston) and send logs to a central system (Datadog Logs, CloudWatch) for analysis and alerting (e.g., on high error rates).
11. Troubleshooting and Caveats
Error: 401 Unauthorized from Sinch: Check SINCH_API_TOKEN, SINCH_SERVICE_PLAN_ID, SINCH_API_BASE_URL in environment variables. Ensure Bearer prefix is correct.
Error: 400 Bad Request from Sinch (e.g., INVALID_PARAMETER_VALUE): Check payload format (E.164 numbers, public mediaUrl), required fields. Examine details in the error response. Check against Sinch XMS API docs.
Error: 403 Forbidden (MMS related): Verify SINCH_NUMBER is MMS-enabled, account supports MMS, destination is supported.
Error: Media Not Displaying: Ensure mediaUrl is public and accessible. Check file type/size limits.
API Route Returns 500 Internal Server Error: Check server logs for exceptions (often missing/undefined environment variables).
Caveat: Sinch API Rate Limits: Sinch enforces limits. Implement client-side rate limiting and consider retries with backoff if needed. Expect 429 Too Many Requests.
Caveat: Asynchronous Delivery: A successful API response (201 Created) means Sinch accepted the batch, not that the MMS was delivered. Use Delivery Reports for status updates.
Caveat: MMS Regional Availability: MMS functionality is only available in the US region and must be explicitly enabled by your Sinch account manager.1 Attempting to send MMS without enablement or from non-US regions will result in API errors. For other regions, use SMS or alternative channels.
12. Deployment and CI/CD
12.1 Deployment Platforms: Vercel and Netlify are excellent choices for Next.js.
12.2 Environment Variables: Configure SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_NUMBER, SINCH_API_BASE_URL securely in your hosting provider's settings. Do not commit .env.local.
12.3 Deployment Process (Example with Vercel):
Push code to Git (ensure .env.local is gitignored).
Connect Git repo to Vercel.
Configure Project Name and Root Directory.
Crucially: Add the four Sinch variables in Vercel's Environment Variables settings.
Deploy.
12.4 CI/CD:
Git integration enables automatic deployments on push.
Integrate automated testing (Jest, Cypress) in your build pipeline (npm test).
Use platform features for rollbacks if needed.
JSON
// package.json (example scripts){"scripts":{"dev":"next dev","build":"next build","start":"next start","lint":"next lint","test":"jest"// Or your test runner command}}
13. Verification and Testing
13.1 Manual Verification:
Run locally: npm run dev.
Open http://localhost:3000.
Fill form with a valid test number (E.164), message, and a publicly accessible image URL.
Submit.
Check frontend for success/error message (with Batch ID on success).
Check test phone for MMS (text + image).
Check server console logs for API route output.
Check Sinch Dashboard API logs.
13.2 Automated Testing (Concepts & Examples):
Unit Tests (API Route): Use Jest to test pages/api/send-mms.js. Mock axios/fetch to simulate Sinch responses (success/errors) and assert handler logic.
JavaScript
// Example using Jest and mocking axios (requires setup)// __tests__/api/send-mms.test.js (simplified concept)import handler from'../../pages/api/send-mms';
import axios from'axios';
jest.mock('axios'); // Mock the axios module// Mock environment variables before tests run
process.env.SINCH_SERVICE_PLAN_ID = 'test-plan-id';
process.env.SINCH_API_TOKEN = 'test-api-token';
process.env.SINCH_NUMBER = '+15550001111';
process.env.SINCH_API_BASE_URL = 'https://fake.sinch.api';
describe('/api/send-mms handler', () => {
let req, res;
beforeEach(() => {
jest.clearAllMocks();
req = { method: 'POST', body: { /* ... valid body ... */ } };
res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), setHeader: jest.fn().mockReturnThis() };
// Default mock for successful Sinch call (201 Created)
axios.post.mockResolvedValue({ status: 201, data: { id: 'test-batch-id' } });
});
it('should return 405 if method is not POST', async () => {
req.method = 'GET';
awaithandler(req, res);
expect(res.status).toHaveBeenCalledWith(405);
});
it('should return 400 if required fields are missing', async () => {
req.body = { to: '+123' }; // Invalid/missing fieldsawaithandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
});
it('should call Sinch API with correct payload and return 200 on valid request', async () => {
req.body = { to: '+15552223333', body: 'Test', mediaUrl: 'https://valid.url/img.jpg' };
awaithandler(req, res);
expect(axios.post).toHaveBeenCalledWith(
expect.stringContaining('/xms/v1/test-plan-id/batches'), // Check endpoint
expect.objectContaining({ from: '+15550001111', to: ['+15552223333'] }), // Check payload essentials
expect.any(Object) // Check config object presence
);
expect(res.status).toHaveBeenCalledWith(200); // API route returns 200 to clientexpect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: true, batchId: 'test-batch-id' }));
});
it('should handle Sinch API errors by forwarding status and details', async () => {
const errorResponse = { response: { status: 401, data: { error: 'Auth failed' } } };
axios.post.mockRejectedValue(errorResponse);
req.body = { to: '+15552223333', body: 'Test', mediaUrl: 'https://valid.url/img.jpg' };
awaithandler(req, res);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Failed to send MMS via Sinch.', details: { error: 'Auth failed' } }));
});
// Add more tests...
});
Integration Tests (Frontend-API): Use React Testing Library or Cypress to test form submission and handling of API responses (mocking fetch or using cy.intercept).
End-to-End (E2E) Tests: Use Cypress/Playwright sparingly against a test environment to simulate full user flow.
13.3 Verification Checklist:
Project created, dependencies installed.
.env.local created with correct Sinch credentials/number (and not committed to Git).
.env.local listed in .gitignore.
API route pages/api/send-mms.js implemented.
Frontend page pages/index.js implemented.
Application runs locally (npm run dev).
Form submission triggers POST to API route (check browser dev tools).
API route logs expected output (request received, Sinch response/error).
Successful submission shows success message with Batch ID on frontend.
MMS (text + media) received on test phone number.
Invalid input (e.g., bad phone number, missing URL) shows error message on frontend.
API errors (simulated or real, e.g., bad token) show error message on frontend.
(Optional) Automated tests pass.
(Deployment) Environment variables configured correctly in hosting.
Node.js LTS Schedule: Node.js 22 (Jod) is Active LTS until October 2025, then Maintenance LTS until April 2027. Node.js 20 (Iron) remains in Maintenance LTS until April 2026. Source: https://nodejs.org/en/about/previous-releases
Next.js Version: This guide is compatible with Next.js 14.x and 15.x. As of October 2025, Next.js 15.5 is the latest stable version with Turbopack builds in beta.
Sinch XMS API: The /xms/v1/{service_plan_id}/batches endpoint supports SMS, MMS (US only), and binary messages. MMS is sent by including parameters.media_body.url in the payload.
Footnotes
Sinch. "SMS API Reference - MMS Support." Sinch Developer Documentation. Accessed October 2025. https://developers.sinch.com/docs/sms/api-reference/ - "If you are in the US region, you are also able to send and receive MMS messages. To enable MMS functionality, contact your account manager." ↩↩2↩3
Frequently asked questions
Use a Next.js API route as your backend to handle communication with the Sinch XMS API. Create a form on your frontend to collect recipient details, message body, and media URL, then send a POST request to the API route, which will interact with Sinch to send the MMS.
The Sinch XMS API is the core service that handles sending MMS messages. Your Next.js API route interacts with its /batches endpoint, sending a specifically formatted JSON payload to trigger the message dispatch.
Storing sensitive information like API tokens directly in your code is a security risk. Environment variables (like those in .env.local) provide a secure way to manage and access these credentials without exposing them in your codebase.
Input validation is crucial on both the client-side (in your form) and server-side (in your API route). This prevents bad data from reaching the Sinch API and causing errors. Validate for missing fields, correct phone number format (E.164), and valid media URLs.
Yes, the built-in fetch API can be used instead of axios to make the API call to Sinch. The core logic for constructing the request remains the same, just the syntax for making the HTTP request will differ slightly.
Implement error handling in your API route to catch potential errors during the API call to Sinch. Log the errors server-side, return specific error codes/messages to the frontend, and consider implementing retry mechanisms with exponential backoff for transient errors.
The API route acts as your serverless backend. It receives requests from the frontend, constructs the payload for the Sinch API (including recipient, message, and media URL), makes the API call, and returns the result (success or error) to the frontend.
The payload should be a JSON object sent to the Sinch /batches endpoint. It must include the recipient's number (to), the message body (body), and the publicly accessible mediaUrl. Authentication is handled via headers, using Bearer authorization with your Sinch API token.
You'll need a Sinch account with an MMS-enabled number, your Sinch API credentials (Service Plan ID and API Token), Node.js and npm/yarn, a publicly accessible media URL, and the Next.js project setup as described in the article.
Use environment variables to store API credentials, implement thorough input validation on both frontend and backend, enforce HTTPS for all communication, consider rate limiting, and use authentication/authorization to protect your API route if needed.
Log in to your Sinch Dashboard and navigate to the SMS/Messaging API section. Find the REST API configuration area to locate your Service Plan ID and API Token. Copy these securely.
Sinch requires the E.164 format for phone numbers (e.g., +12125551234). Validate and enforce this format on both your frontend and API route to prevent errors.
Verify that the mediaUrl you're providing is publicly accessible by Sinch. Also, check Sinch's documentation for any file type or size restrictions that might be causing the issue. If the URL requires any sort of authentication it will not be fetchable by Sinch.
Double-check your SINCH_API_TOKEN, SINCH_SERVICE_PLAN_ID, and SINCH_API_BASE_URL in your .env.local file and your deployment environment variables. Ensure the Authorization header has the correct Bearer prefix and that these values match exactly what is in your Sinch dashboard.
It means Sinch has successfully received your MMS request and added it to its queue for processing. It does not guarantee immediate delivery. You'll need to rely on Sinch's delivery reports for actual delivery status.
Learn how to send MMS multimedia messages with images and videos using Sinch XMS API, Next.js, and Node.js. This complete tutorial shows you how to build a secure Next.js API route that handles MMS delivery, create a React form for message composition, and deploy a production-ready messaging solution.
You'll build a web application with a user-friendly interface to capture recipient phone numbers, message content, and media URLs. The React frontend communicates with a Next.js API route that integrates with the Sinch XMS API to send MMS messages reliably.
Project Goals:
Technology Stack:
/batchesendpoint.fetchAPI.System Architecture:
Prerequisites:
Expected Outcome:
By the end of this guide, you'll have a functional Next.js application with a form for MMS submission. Entering a recipient number, message, and media URL triggers an API call to your backend, which uses the Sinch XMS API to send the MMS message. You'll also implement error handling and user feedback.
1. Next.js Project Setup for Sinch MMS Integration
Create a new Next.js application and configure the dependencies needed for MMS messaging with Sinch.
1.1 Create the Next.js App:
Open your terminal and run the following command, replacing
sinch-mms-nextjswith your desired project name:Follow the prompts. Select these options:
src/directory: No (we'll use the defaultpagesstructure)pagesdirectory and API routes for clarity)1.2 Navigate into the Project Directory:
cd sinch-mms-nextjs1.3 Install Dependencies:
Install
axiosto make HTTP requests from your backend API route to Sinch. Skip this step if you prefer the built-infetchAPI.1.4 Configure Environment Variables:
Never hardcode API keys. Use environment variables instead. Create a
.env.localfile in your project root:touch .env.localOpen
.env.localand add the following lines, replacing the placeholder values with your actual Sinch credentials and number:Explanation of Environment Variables:
SINCH_SERVICE_PLAN_ID: Your unique identifier for the API plan. Find this in the Sinch Customer Dashboard.SINCH_API_TOKEN: Secret token to authenticate your API requests. Find this in the Sinch Customer Dashboard. Treat this like a password.SINCH_NUMBER: MMS-capable phone number from your Sinch account used as the sender ("From" number). Must be in E.164 format (e.g.,+12125551234).SINCH_API_BASE_URL: Base URL for the Sinch Messaging API specific to your region. Ensure this matches your account setup.Important: Add
.env.localto your.gitignorefile to prevent accidentally committing your credentials.create-next-appusually does this automatically.Project Structure (Relevant Files):
This structure separates concerns:
pages/api/contains server-side API routes (your Node.js backend), whilepages/contains client-side React components. Next.js automatically routes requests to/api/send-mmsto thesend-mms.jshandler.2. Building the Sinch MMS API Route in Next.js
Create the backend logic within a Next.js API route. This route receives requests from your frontend, constructs the MMS payload for Sinch XMS API, and makes the API call.
2.1 Create the API Route File:
Create a new file:
pages/api/send-mms.js2.2 Implement the API Handler:
Paste the following code into
pages/api/send-mms.js:// pages/api/send-mms.js import axios from 'axios'; // Or use built-in fetch // Ensure environment variables are loaded (Next.js handles this automatically) const SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID; const API_TOKEN = process.env.SINCH_API_TOKEN; const SINCH_NUMBER = process.env.SINCH_NUMBER; const SINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL; export default async function handler(req, res) { // 1. Only allow POST requests if (req.method !== 'POST') { res.setHeader('Allow', ['POST']); return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); } // 2. Basic Input Validation const { to, body, mediaUrl } = req.body; if (!to || !body || !mediaUrl) { return res.status(400).json({ error: 'Missing required fields: to, body, mediaUrl' }); } // Validate phone number format (basic E.164 check) const e164Regex = /^\+[1-9]\d{1,14}$/; if (!e164Regex.test(to)) { return res.status(400).json({ error: 'Invalid "to" phone number format. Use E.164 (e.g., +12125551234).' }); } // Validate media URL format (basic check) try { new URL(mediaUrl); } catch (error) { return res.status(400).json({ error: 'Invalid "mediaUrl" format.' }); } // Optional: Validate media file extension const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov']; const urlPath = new URL(mediaUrl).pathname.toLowerCase(); const hasValidExtension = validExtensions.some(ext => urlPath.endsWith(ext)); if (!hasValidExtension) { return res.status(400).json({ error: 'Invalid media file type. Supported: JPEG, PNG, GIF, MP4, MOV.' }); } // 3. Construct the Sinch XMS API Payload for MMS Batch // The /batches endpoint is used for sending SMS/MMS. const payload = { from: SINCH_NUMBER, to: [to], // API expects an array of recipients body: body, parameters: { // Parameters specific to MMS often go here media_body: { url: mediaUrl, // Optional: message if URL cannot be fetched, filename // message: "Could not display media content.", // filename: "image.jpg" } }, // Optional: type: 'mt_media' // Explicitly setting type might be required by some Sinch setups // Optional: delivery_report: 'full' // Request delivery reports }; // Optional parameters explained: // - delivery_report: 'none', 'summary', or 'full' – controls delivery status callbacks // - type: 'mt_media' – explicitly marks message as MMS (usually auto-detected) // - callback_url: URL where Sinch sends delivery reports (requires webhook endpoint) // To receive delivery reports, set delivery_report: 'full' and implement a webhook endpoint // 4. Prepare API Request Options // Using the XMS (Cross-channel Messaging) API endpoint for batches const API_ENDPOINT = `${SINCH_API_BASE_URL}/xms/v1/${SERVICE_PLAN_ID}/batches`; const config = { headers: { 'Authorization': `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json', }, }; // 5. Send the request to Sinch try { console.log(`Sending MMS batch request to ${API_ENDPOINT}`); // WARNING: Do NOT log the full payload in production – it contains sensitive recipient numbers and message content. // Log only metadata or sanitized data. // console.log('Payload (DEBUG):', JSON.stringify(payload, null, 2)); const sinchResponse = await axios.post(API_ENDPOINT, payload, config); console.log('Sinch API Response Status:', sinchResponse.status); // console.log('Sinch API Response Data:', sinchResponse.data); // Avoid logging full response data in production // 6. Handle Sinch Response (Success) // Sinch typically returns 201 Created for successful batch submission. if (sinchResponse.status === 201) { return res.status(200).json({ // Return 200 OK to the client success: true, message: 'MMS submitted successfully to Sinch.', batchId: sinchResponse.data.id, // Include batch ID for tracking }); } else { // Handle unexpected success status codes (201 is standard) console.warn('Received unexpected success status from Sinch:', sinchResponse.status); return res.status(500).json({ error: 'Received unexpected success status from Sinch API.', details: sinchResponse.data }); } } catch (error) { // 7. Handle Errors console.error('Error sending MMS via Sinch:', error); if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.error('Sinch API Error Status:', error.response.status); console.error('Sinch API Error Data:', error.response.data); // Forward Sinch's error status and data if available return res.status(error.response.status || 500).json({ error: 'Failed to send MMS via Sinch.', details: error.response.data || 'No details provided.', }); } else if (error.request) { // The request was made but no response was received console.error('Sinch API No Response:', error.request); return res.status(504).json({ error: 'No response received from Sinch API.' }); } else { // Something happened in setting up the request that triggered an Error console.error('Sinch API Request Setup Error:', error.message); return res.status(500).json({ error: 'Error setting up request to Sinch API.', details: error.message }); } } }Code Explanation:
to,body,mediaUrl) and validates the phone number (E.164), media URL format, and file extension.payloadfor the Sinch/xms/v1/.../batchesendpoint.from: Your Sinch number.to: An array containing the recipient's E.164 number.body: The text part of the message.parameters.media_body.url: Specifies the media URL for MMS via the XMS API. This URL must be publicly accessible.AuthorizationandContent-Typeheaders.axios.post(orfetch) to send the request. Includes basic logging (with a warning about logging sensitive data in production).201 Createdstatus from Sinch. Returns a200 OKresponse to the frontend with success status and the SinchbatchId.Common Sinch Error Responses:
unauthorized.env.localsyntax_invalid_parameter_format+12125551234)forbiddensyntax_constraint_violationtoo_many_requests3. Creating the React Frontend for MMS Composition
Build a React component on a Next.js page to collect user input (recipient, message, media URL) and trigger your MMS API route.
3.1 Modify the Index Page:
Replace the contents of
pages/index.jswith the following code:// pages/index.js import { useState } from 'react'; import Head from 'next/head'; export default function HomePage() { const [to, setTo] = useState(''); const [body, setBody] = useState('Check out this cool image!'); const [mediaUrl, setMediaUrl] = useState(''); const [statusMessage, setStatusMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (event) => { event.preventDefault(); setIsLoading(true); setStatusMessage(''); // Basic frontend validation (optional, enhance as needed) if (!to || !body || !mediaUrl) { setStatusMessage('Error: Please fill in all fields.'); setIsLoading(false); return; } if (!mediaUrl.startsWith('http://') && !mediaUrl.startsWith('https://')) { setStatusMessage('Error: Media URL must start with http:// or https://'); setIsLoading(false); return; } try { const response = await fetch('/api/send-mms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ to, body, mediaUrl }), }); const data = await response.json(); if (response.ok) { // Check if status code is 2xx setStatusMessage(`Success! MMS submitted. Batch ID: ${data.batchId}`); // Optional: Clear form on success // setTo(''); // setBody('Check out this cool image!'); // setMediaUrl(''); } else { // Handle errors reported by our API route setStatusMessage(`Error: ${data.error} ${data.details ? `(${JSON.stringify(data.details)})` : ''}`); } } catch (error) { console.error('Frontend Error sending MMS:', error); setStatusMessage('Error: Failed to send request. Check console.'); } finally { setIsLoading(false); } }; // Basic inline styles for demonstration const styles = { container: { padding: '2rem', maxWidth: '500px', margin: 'auto', fontFamily: 'sans-serif' }, form: { display: 'flex', flexDirection: 'column', gap: '1rem' }, label: { display: 'flex', flexDirection: 'column', gap: '0.5rem' }, input: { padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }, button: { padding: '0.75rem', border: 'none', borderRadius: '4px', backgroundColor: '#0070f3', color: 'white', cursor: 'pointer', fontSize: '1rem' }, buttonDisabled: { backgroundColor: '#ccc', cursor: 'not-allowed' }, status: { marginTop: '1rem', padding: '0.5rem', borderRadius: '4px', wordBreak: 'break-word' }, statusSuccess: { backgroundColor: '#d4edda', color: '#155724' }, statusError: { backgroundColor: '#f8d7da', color: '#721c24' }, }; return ( <div style={styles.container}> <Head> <title>Send Sinch MMS with Next.js</title> <meta name="description" content="Send MMS messages using Sinch API and Next.js" /> <link rel="icon" href="/favicon.ico" /> </Head> <h1>Send MMS via Sinch</h1> <form onSubmit={handleSubmit} style={styles.form}> <label style={styles.label}> Recipient Phone Number (E.164): <input type="tel" value={to} onChange={(e) => setTo(e.target.value)} placeholder="+12125551234" required style={styles.input} /> </label> <label style={styles.label}> Message Body: <textarea value={body} onChange={(e) => setBody(e.target.value)} required rows={3} style={styles.input} /> </label> <label style={styles.label}> Public Media URL (Image/Video): <input type="url" value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} placeholder="https://example.com/image.jpg" required style={styles.input} /> </label> <button type="submit" disabled={isLoading} style={{ ...styles.button, ...(isLoading ? styles.buttonDisabled : {}) }} > {isLoading ? 'Sending...' : 'Send MMS'} </button> </form> {statusMessage && ( <div style={{ ...styles.status, ...(statusMessage.startsWith('Success') ? styles.statusSuccess : styles.statusError) }}> {statusMessage} </div> )} </div> ); }Code Explanation:
useStatefor form inputs (to,body,mediaUrl), loading state (isLoading), and feedback (statusMessage).handleSubmitFunction:fetchto POST data to/api/send-mms.statusMessagefor success (showing Batch ID) or failure (showing error details from the API).fetch.finallyblock.statusMessagebelow the form, styled for success/error. Disables the button during loading.metaandlinktags for proper SEO.4. Integrating with Sinch (Credentials & Dashboard)
This section reiterates how to obtain and securely manage your Sinch API credentials, which we've already placed in
.env.local.4.1 Obtaining Credentials:
4.2 Obtaining Your Sinch Number:
+1xxxxxxxxxx).4.3 Secure Storage:
SINCH_SERVICE_PLAN_ID,SINCH_API_TOKEN,SINCH_NUMBER) into your.env.localfile..env.localor any file containing your API token to version control. Ensure.env*.localis in your.gitignorefile.Environment Variable Configuration by Platform:
5. Error Handling and Logging
We've implemented error handling in both the API route and the frontend.
API Route (
pages/api/send-mms.js):400 Bad Requestfor missing/invalid inputs.405 Method Not Allowedfor non-POST requests.axios.postcall, logs details server-side (console.error), and returns specific status codes/messages to the frontend.console.logandconsole.error. Important: In a production environment, replaceconsole.log/console.errorwith a robust logging library (e.g., Pino, Winston). Critically, ensure that any debug logging (like the commented-out payload log in the API route) is removed or heavily sanitized in production to avoid logging sensitive information like recipient phone numbers or message content.Frontend (
pages/index.js):fetchcall.Retry Mechanisms (Advanced):
For production, consider implementing retries with exponential backoff in the API route for transient network errors or temporary Sinch API issues (e.g., 5xx errors). Libraries like
axios-retrycan help with this implementation.6. Database Schema and Data Layer (Not Applicable)
This guide focuses on the stateless action of sending an MMS and does not require a database. For applications needing message history or tracking, integrate a database (e.g., PostgreSQL, MongoDB) and data layer (e.g., Prisma, TypeORM).
7. Adding Security Features
/api/send-mmsto prevent abuse. Use libraries likerate-limiter-flexibleor platform features.mediaUrlis fetched by Sinch. While basic URL validation is in place, ensure your validation prevents users from submitting malicious or invalid URLs. Robust URL validation and file extension checking protect against abuse.8. Handling Special Cases (MMS Specific)
Content-Typeheaders.9. Implementing Performance Optimizations (Limited Scope)
Performance is mainly dictated by the Sinch API response time.
10. Adding Monitoring, Observability, and Analytics (Basic Concepts)
For production:
/api/healthendpoint.11. Troubleshooting and Caveats
401 Unauthorizedfrom Sinch: CheckSINCH_API_TOKEN,SINCH_SERVICE_PLAN_ID,SINCH_API_BASE_URLin environment variables. EnsureBearerprefix is correct.400 Bad Requestfrom Sinch (e.g.,INVALID_PARAMETER_VALUE): Check payload format (E.164 numbers, publicmediaUrl), required fields. Examinedetailsin the error response. Check against Sinch XMS API docs.403 Forbidden(MMS related): VerifySINCH_NUMBERis MMS-enabled, account supports MMS, destination is supported.mediaUrlis public and accessible. Check file type/size limits.500 Internal Server Error: Check server logs for exceptions (often missing/undefined environment variables).429 Too Many Requests.201 Created) means Sinch accepted the batch, not that the MMS was delivered. Use Delivery Reports for status updates.12. Deployment and CI/CD
12.1 Deployment Platforms: Vercel and Netlify are excellent choices for Next.js.
12.2 Environment Variables: Configure
SINCH_SERVICE_PLAN_ID,SINCH_API_TOKEN,SINCH_NUMBER,SINCH_API_BASE_URLsecurely in your hosting provider's settings. Do not commit.env.local.12.3 Deployment Process (Example with Vercel):
.env.localis gitignored).12.4 CI/CD:
npm test).// package.json (example scripts) { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "test": "jest" // Or your test runner command } }13. Verification and Testing
13.1 Manual Verification:
npm run dev.http://localhost:3000.13.2 Automated Testing (Concepts & Examples):
Unit Tests (API Route): Use Jest to test
pages/api/send-mms.js. Mockaxios/fetchto simulate Sinch responses (success/errors) and assert handler logic.// Example using Jest and mocking axios (requires setup) // __tests__/api/send-mms.test.js (simplified concept) import handler from '../../pages/api/send-mms'; import axios from 'axios'; jest.mock('axios'); // Mock the axios module // Mock environment variables before tests run process.env.SINCH_SERVICE_PLAN_ID = 'test-plan-id'; process.env.SINCH_API_TOKEN = 'test-api-token'; process.env.SINCH_NUMBER = '+15550001111'; process.env.SINCH_API_BASE_URL = 'https://fake.sinch.api'; describe('/api/send-mms handler', () => { let req, res; beforeEach(() => { jest.clearAllMocks(); req = { method: 'POST', body: { /* ... valid body ... */ } }; res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), setHeader: jest.fn().mockReturnThis() }; // Default mock for successful Sinch call (201 Created) axios.post.mockResolvedValue({ status: 201, data: { id: 'test-batch-id' } }); }); it('should return 405 if method is not POST', async () => { req.method = 'GET'; await handler(req, res); expect(res.status).toHaveBeenCalledWith(405); }); it('should return 400 if required fields are missing', async () => { req.body = { to: '+123' }; // Invalid/missing fields await handler(req, res); expect(res.status).toHaveBeenCalledWith(400); }); it('should call Sinch API with correct payload and return 200 on valid request', async () => { req.body = { to: '+15552223333', body: 'Test', mediaUrl: 'https://valid.url/img.jpg' }; await handler(req, res); expect(axios.post).toHaveBeenCalledWith( expect.stringContaining('/xms/v1/test-plan-id/batches'), // Check endpoint expect.objectContaining({ from: '+15550001111', to: ['+15552223333'] }), // Check payload essentials expect.any(Object) // Check config object presence ); expect(res.status).toHaveBeenCalledWith(200); // API route returns 200 to client expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: true, batchId: 'test-batch-id' })); }); it('should handle Sinch API errors by forwarding status and details', async () => { const errorResponse = { response: { status: 401, data: { error: 'Auth failed' } } }; axios.post.mockRejectedValue(errorResponse); req.body = { to: '+15552223333', body: 'Test', mediaUrl: 'https://valid.url/img.jpg' }; await handler(req, res); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Failed to send MMS via Sinch.', details: { error: 'Auth failed' } })); }); // Add more tests... });Integration Tests (Frontend-API): Use React Testing Library or Cypress to test form submission and handling of API responses (mocking
fetchor usingcy.intercept).End-to-End (E2E) Tests: Use Cypress/Playwright sparingly against a test environment to simulate full user flow.
13.3 Verification Checklist:
.env.localcreated with correct Sinch credentials/number (and not committed to Git)..env.locallisted in.gitignore.pages/api/send-mms.jsimplemented.pages/index.jsimplemented.npm run dev).Related Guides
References
Additional Technical References:
/xms/v1/{service_plan_id}/batchesendpoint supports SMS, MMS (US only), and binary messages. MMS is sent by includingparameters.media_body.urlin the payload.Footnotes
Sinch. "SMS API Reference - MMS Support." Sinch Developer Documentation. Accessed October 2025. https://developers.sinch.com/docs/sms/api-reference/ - "If you are in the US region, you are also able to send and receive MMS messages. To enable MMS functionality, contact your account manager." ↩ ↩2 ↩3