Send MMS with AWS SNS and Next.js: A Developer Guide
A step-by-step guide for building a Next.js application to send MMS messages using AWS Pinpoint SMS and Voice v2 API, AWS SDK v3, and S3 for media storage.
This guide provides a step-by-step walkthrough for building a feature within a Next.js application to send Multimedia Messaging Service (MMS) messages using AWS services. We'll leverage the AWS SDK for JavaScript v3, specifically interacting with the Amazon Pinpoint SMS and Voice v2 API (sms-voice namespace). While the AWS console might still reference SNS for SMS history, this newer Pinpoint API is used for sending MMS messages programmatically. Amazon S3 will be used for media storage.
Project Overview and Goals
Goal: To create a simple Next.js application with a backend API endpoint capable of sending MMS messages containing text and an image hosted on Amazon S3 to a specified phone number.
Problem Solved: This provides a foundational implementation for applications needing to send rich media messages programmatically – useful for notifications, alerts, marketing campaigns, or user interactions involving images or other media.
Technologies:
Next.js: A React framework providing server-side rendering, static site generation, and simplified API route creation. Chosen for its developer experience and ease of building full-stack applications.
AWS SDK for JavaScript v3: The modern AWS SDK, used to interact with AWS services programmatically. We'll use clients for Amazon Pinpoint SMS and Voice v2 (@aws-sdk/client-pinpoint-sms-voice-v2). Chosen for its modularity and async/await support.
Amazon Pinpoint SMS and Voice v2 API: The specific AWS API endpoint (SendMediaMessage) required for sending MMS messages.
Amazon S3: Used to host the media files (e.g., images, videos) that will be included in the MMS messages. Chosen for its scalability, durability, and integration with other AWS services.
Node.js: The runtime environment for Next.js and the AWS SDK.
An AWS account with permissions to manage IAM, S3, and Pinpoint/SNS resources.
Node.js (v18 or later recommended) and npm/yarn installed.
AWS CLI installed and configured (useful for setup and testing).
A basic understanding of Next.js, React, and asynchronous JavaScript.
An MMS-capable phone number obtained through AWS (e.g., a 10DLC number, Toll-Free Number in supported regions like US/Canada).
Note: You will set up the S3 bucket and upload the media file in Section 2 as part of this guide.
Outcome: By the end of this guide, you will have a functional Next.js application with a simple UI allowing users to input a destination phone number, a message body, and a media URL (hosted on S3), which triggers an API call to send an MMS message via AWS.
1. Setting up the Project
Let's initialize our Next.js project and install the necessary dependencies.
Set Up Environment Variables:
Create a file named .env.local in the root of your project. Never commit this file to version control if it contains sensitive credentials.
.env
# AWS Credentials (Best Practice: Use IAM Roles for EC2/ECS/Lambda or temporary credentials)
# For local development, these can be used but are less secure.
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
# AWS Configuration
AWS_REGION=us-east-1 # IMPORTANT: Use the SAME region as your S3 bucket and Origination Number
# Application Specific
# Get this from your AWS Pinpoint/SNS console after acquiring a number
SNS_ORIGINATION_NUMBER=+1xxxxxxxxxx # Your MMS-capable AWS phone number in E.164 format
S3_BUCKET_NAME=your-mms-media-bucket-name # The name of your S3 bucket
Obtaining AWS Credentials:
Navigate to the AWS IAM console.
Create an IAM user with Programmatic access.
Attach necessary permissions (see Step 5).
Record the generated Access Key ID and Secret Access Key.
Security Note: For production deployments on AWS (EC2, Lambda, etc.), use IAM Roles instead of storing long-lived credentials. The SDK automatically uses roles if available. For local development, these environment variables are common, but ensure .env.local is in your .gitignore.
AWS Region: Choose the AWS region where you will create your S3 bucket and where your origination phone number is registered. Consistency is crucial.
Origination Number: Acquire an MMS-capable number through the AWS console (often under Pinpoint or SNS sections depending on your setup). Ensure it's active and MMS is enabled.
S3 Bucket Name: The globally unique name of the S3 bucket you'll create in the next section.
Configure IAM Permissions:
The IAM user (or role) whose credentials your Next.js app uses needs permissions to:
Send MMS messages via Pinpoint SMS/Voice v2.
Read the media file from your S3 bucket.
Create an IAM policy (e.g., NextJsMmsAppPolicy) with the following JSON structure, replacing placeholders:
Replace YOUR_MMS_MEDIA_BUCKET_NAME with your actual bucket name.
Attach this policy to the IAM user you created in step 4 (or to the IAM role your deployment environment uses).
Why these permissions?sms-voice:SendMediaMessage allows the application to use the specific API call for MMS. s3:GetObject enables the Pinpoint service (acting on behalf of your call) to retrieve the media file from your bucket when constructing the MMS.
2. AWS Resource Setup (S3 Bucket and Media)
MMS messages require media files to be hosted somewhere accessible. AWS S3 is the standard choice.
Create S3 Bucket:
Navigate to the AWS S3 console.
Click ""Create bucket"".
Enter a globally unique bucket name (this will be your S3_BUCKET_NAME in .env.local).
Select the same AWS Region as specified in your .env.local and where your Origination Number resides. This is critical for MMS sending.
Keep default settings for Block Public Access (usually enabled – we will use IAM permissions for access, not public URLs).
Click ""Create bucket"".
Upload Media File:
Navigate into your newly created bucket in the S3 console.
Click ""Upload"".
Add your media file (e.g., cat-image.jpg). Ensure it meets AWS MMS requirements (check Supported file types and sizes). Common formats like JPEG, PNG, GIF (under limits) are usually fine.
Click ""Upload"".
Get Media File S3 URI:
Once uploaded, select the media file in the S3 console.
Find the ""S3 URI"" (it looks like s3://your-mms-media-bucket-name/cat-image.jpg). You will need this URI later for the API call. Note: This is not the public Object URL.
3. Implementing the Backend API Route
We'll create a Next.js API route to handle the MMS sending logic.
Create API Route File:
Create the file src/app/api/send-mms/route.ts (if using App Router) or src/pages/api/send-mms.ts (if using Pages Router). This example uses the App Router structure.
Implement the API Handler:
Paste the following code into src/app/api/send-mms/route.ts:
TypeScript
// src/app/api/send-mms/route.tsimport { NextRequest, NextResponse } from'next/server';
import {
PinpointSMSVoiceV2Client,
SendMediaMessageCommand,
SendMediaMessageCommandInput,
} from'@aws-sdk/client-pinpoint-sms-voice-v2';
// Ensure environment variables are loaded (especially outside Vercel)// require('dotenv').config({ path: '.env.local' }); // Uncomment if needed locally and not using Next.js auto-loading// Retrieve environment variables (checks happen within the handler)const { AWS_REGION, SNS_ORIGINATION_NUMBER } = process.env;
// Create the Pinpoint SMS Voice v2 Client// Credentials will be automatically sourced from environment variables// (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or IAM role if deployed on AWS infrastructure.const pinpointClient = newPinpointSMSVoiceV2Client({
region: AWS_REGION, // SDK will use AWS_REGION env var if not explicitly passed here
});
exportasyncfunctionPOST(request: NextRequest) {
console.log('Received request to /api/send-mms');
// Validate required environment variables *per request*if (!AWS_REGION || !SNS_ORIGINATION_NUMBER) {
console.error('Server configuration error: Missing AWS_REGION or SNS_ORIGINATION_NUMBER');
returnNextResponse.json(
{ error: 'Server configuration error: Missing AWS environment variables.' },
{ status: 500 }
);
}
let requestBody;
try {
requestBody = await request.json();
console.log('Request body:', requestBody);
} catch (error) {
console.error('Error parsing request body:', error);
returnNextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}
const { destinationPhoneNumber, messageBody, mediaUrl } = requestBody;
// Basic Input Validationif (!destinationPhoneNumber || !mediaUrl) {
returnNextResponse.json(
{ error: 'Missing required fields: destinationPhoneNumber and mediaUrl are required.' },
{ status: 400 }
);
}
// More robust validation could be added here (e.g., E.164 format check for phone number, S3 URI format check)if (!/^s3:\/\/[^\/]+\/.+/.test(mediaUrl)) {
returnNextResponse.json(
{ error: 'Invalid mediaUrl format. Must be an S3 URI (e.g., s3://bucket-name/key-name.jpg).' },
{ status: 400 }
);
}
if (!/^\+\d{11,15}$/.test(destinationPhoneNumber)) {
returnNextResponse.json(
{ error: 'Invalid destinationPhoneNumber format. Must be E.164 format (e.g., +12223334444).' },
{ status: 400 }
);
}
constparams: SendMediaMessageCommandInput = {
DestinationPhoneNumber: destinationPhoneNumber, // Provided in requestOriginationIdentity: SNS_ORIGINATION_NUMBER, // From environment variableMessageBody: messageBody || '', // Optional message body from requestMediaUrls: [mediaUrl], // S3 URI from request (must be an array)// ConfigurationSetName: 'YourOptionalConfigSetName' // If using configuration sets
};
console.log('Attempting to send MMS with params:', params);
try {
const command = newSendMediaMessageCommand(params);
const response = await pinpointClient.send(command);
console.log('MMS sent successfully:', response);
// The response contains the MessageIdreturnNextResponse.json({
success: true,
messageId: response.MessageId,
message: 'MMS sent successfully',
}, { status: 200 });
} catch (error: any) {
console.error('Failed to send MMS:', error);
// Provide more specific feedback if possiblelet errorMessage = 'Failed to send MMS.';
let statusCode = 500;
if (error.name === 'ValidationException') {
errorMessage = `Invalid request parameter: ${error.message}`;
statusCode = 400;
} elseif (error.name === 'AccessDeniedException') {
errorMessage = `Permission denied. Check IAM permissions for sms-voice:SendMediaMessage and s3:GetObject. ${error.message}`;
statusCode = 403;
} elseif (error.name === 'ResourceNotFoundException') {
errorMessage = `Resource not found (e.g., S3 object or Origination Number): ${error.message}`;
statusCode = 404;
} elseif (error.name === 'ThrottlingException') {
errorMessage = `Request throttled by AWS: ${error.message}`;
statusCode = 429;
} // Add more specific error handling as neededreturnNextResponse.json({ error: errorMessage, details: error.message }, { status: statusCode });
}
}
Code Explanation:
Imports NextRequest, NextResponse and necessary AWS SDK v3 components.
Retrieves AWS configuration and the origination number from environment variables.
Instantiates the PinpointSMSVoiceV2Client. Credentials are automatically handled by the SDK based on environment variables or IAM roles.
Defines an asynchronous POST handler function as required by Next.js App Router API routes.
Checks for required environment variables (AWS_REGION, SNS_ORIGINATION_NUMBER) within the handler to ensure configuration is present for each request.
Parses the incoming JSON request body (destinationPhoneNumber, messageBody, mediaUrl).
Performs basic input validation (presence checks, S3 URI format, E.164 format).
Constructs the SendMediaMessageCommandInput object using data from the request and environment variables. Note that MediaUrls must be an array of S3 URIs.
Creates a SendMediaMessageCommand with the parameters.
Calls pinpointClient.send(command) within a try...catch block to execute the API call.
On success, logs the response and returns a JSON object with success: true and the messageId.
On failure, logs the error and returns a JSON error response with an appropriate status code and message, attempting to provide specific feedback based on the AWS error type.
4. Implementing the Frontend UI
Now, let's create a simple React component to interact with our API endpoint.
Implement the Component:
Paste the following code:
TypeScript
// src/components/MmsSender.tsx'use client'; // Required for components with interactivity (useState, onClick) in App RouterimportReact, { useState } from'react';
exportdefaultfunctionMmsSender() {
const [destinationPhoneNumber, setDestinationPhoneNumber] = useState('');
const [messageBody, setMessageBody] = useState('');
const [mediaUrl, setMediaUrl] = useState(''); // Should be an S3 URI entered by the userconst [statusMessage, setStatusMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
consthandleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setStatusMessage('Sending MMS...');
// Basic validation reminderif (!destinationPhoneNumber || !mediaUrl) { // Check if mediaUrl is providedsetStatusMessage('Error: Destination Phone Number and Media URL (S3 URI) are required.');
setIsLoading(false);
return;
}
// E.164 format validation reminderif (!/^\+\d{11,15}$/.test(destinationPhoneNumber)) {
setStatusMessage('Error: Invalid phone number format. Use E.164 (e.g., +12223334444).');
setIsLoading(false);
return;
}
// S3 URI format validation reminder (basic)if (!/^s3:\/\/[^\/]+\/.+/.test(mediaUrl)) {
setStatusMessage('Error: Invalid Media URL format. Must be an S3 URI (e.g., s3://bucket-name/key-name.jpg).');
setIsLoading(false);
return;
}
try {
const response = awaitfetch('/api/send-mms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
destinationPhoneNumber,
messageBody,
mediaUrl: mediaUrl, // Use the S3 URI from state
}),
});
const result = await response.json();
if (response.ok && result.success) {
setStatusMessage(`MMS sent successfully! Message ID: ${result.messageId}`);
// Clear form on successsetDestinationPhoneNumber('');
setMessageBody('');
setMediaUrl('');
} else {
setStatusMessage(`Error: ${result.error || 'Failed to send MMS.'}${result.details ? `(${result.details})` : ''}`);
}
} catch (error) {
console.error('Network or unexpected error:', error);
setStatusMessage('Error: Failed to connect to the API.');
} finally {
setIsLoading(false);
}
};
return (
<divclassName=""max-w-mdmx-automt-10p-6borderrounded-lgshadow-lg""><h2className=""text-2xlfont-semiboldmb-4text-center"">Send MMS via AWS</h2><formonSubmit={handleSubmit}><divclassName=""mb-4""><labelhtmlFor=""destinationPhoneNumber"" className=""blocktext-smfont-mediumtext-gray-700mb-1"">
Destination Phone Number (E.164):
</label><inputtype=""tel""
id=""destinationPhoneNumber""
value={destinationPhoneNumber}onChange={(e) => setDestinationPhoneNumber(e.target.value)}
placeholder=""+12223334444""
required
className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500""
/>
</div><divclassName=""mb-4""><labelhtmlFor=""messageBody"" className=""blocktext-smfont-mediumtext-gray-700mb-1"">
Message Body (Optional):
</label><textareaid=""messageBody""
value={messageBody}onChange={(e) => setMessageBody(e.target.value)}
rows={3}
className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500""
/>
</div><divclassName=""mb-4""><labelhtmlFor=""mediaUrl"" className=""blocktext-smfont-mediumtext-gray-700mb-1"">
Media S3 URI:
</label><inputtype=""text""
id=""mediaUrl""
value={mediaUrl}onChange={(e) => setMediaUrl(e.target.value)}
placeholder=""s3://your-bucket/image.jpg""
required
className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500""
/>
<pclassName=""text-xstext-gray-500mt-1"">Must be the S3 URI of the media file (e.g., s3://bucket-name/key).</p></div><buttontype=""submit""
disabled={isLoading}className=""w-fullpy-2px-4bg-indigo-600text-whitefont-semiboldrounded-mdshadowhover:bg-indigo-700focus:outline-nonefocus:ring-2focus:ring-offset-2focus:ring-indigo-500disabled:opacity-50""
>
{isLoading ? 'Sending...' : 'Send MMS'}
</button></form>
{statusMessage && (
<pclassName={`mt-4text-sm ${statusMessage.startsWith('Error:') ? 'text-red-600' : 'text-green-600'}`}>
{statusMessage}
</p>
)}
</div>
);
}
Code Explanation: Uses useState to manage form inputs and status messages. The handleSubmit function prevents default form submission, sets loading state, performs basic frontend validation checks (including for the S3 URI format), sends a POST request to /api/send-mms using fetch, includes the form data (using the user-provided mediaUrl) in the JSON body, and updates the status message based on the API response.
Use the Component:
Replace the content of src/app/page.tsx (or src/pages/index.tsx) with:
Our API route includes basic error handling and logging:
Input Validation: Checks for required fields and basic format validity (S3 URI, E.164) on the backend. This prevents unnecessary calls to AWS.
AWS SDK Errors: The try...catch block around pinpointClient.send() catches errors from the AWS API.
Specific Error Handling: We attempt to identify common errors (ValidationException, AccessDeniedException, ResourceNotFoundException, ThrottlingException) and return more informative messages and appropriate HTTP status codes (400, 403, 404, 429).
Generic Errors: A fallback 500 Internal Server Error is returned for unexpected issues.
Logging:console.log and console.error are used to log request details, success messages, and errors on the server side (visible in your terminal during local development or in your deployment provider's logs).
Improvements:
Structured Logging: Use a library like pino or winston for structured JSON logging, which is easier to parse and analyze in production logging systems (e.g., CloudWatch Logs, Datadog).
Error Tracking Services: Integrate services like Sentry or Bugsnag to capture and aggregate errors automatically.
Retry Mechanisms: While the AWS SDK has built-in retries for transient network issues, you might implement application-level retries with exponential backoff (using libraries like async-retry) for specific scenarios (e.g., temporary throttling if you anticipate bursts), although this adds complexity.
6. Security Considerations
IAM Least Privilege: Ensure the IAM user/role used by the application has only the permissions required (sms-voice:SendMediaMessage, s3:GetObject on the specific bucket). Avoid using AdministratorAccess.
Input Sanitization/Validation: The backend API route must rigorously validate all inputs (destinationPhoneNumber, messageBody, mediaUrl) to prevent injection attacks or malformed requests. Check formats (E.164, S3 URI), lengths, and potentially disallowed characters.
Rate Limiting: Implement rate limiting on the /api/send-mms endpoint to prevent abuse and control costs. Libraries like rate-limiter-flexible or platform features (Vercel, Cloudflare) can be used.
Authentication/Authorization: This guide assumes an open endpoint. In a real application, protect this API route. Ensure only authenticated and authorized users/systems can trigger MMS sending. Implement session management, JWT, or API keys depending on your architecture.
Secrets Management:Never commit secrets (.env.local) to Git. Use environment variables provided by your hosting platform (Vercel, Netlify, AWS Systems Manager Parameter Store, Secrets Manager) for production deployments.
S3 Bucket Policy: Double-check that your S3 bucket is not publicly accessible unless intended. Rely on IAM permissions for the application to access media.
7. Testing
Local Development:
Run npm run dev or yarn dev.
Open http://localhost:3000 in your browser.
Fill in a verified destination phone number (if your AWS account is still in the SMS/MMS sandbox) or any valid number (if out of the sandbox). Use the E.164 format (e.g., +15551234567).
Enter an optional message.
Enter the correct S3 URI for your uploaded media file (e.g., s3://your-mms-media-bucket-name/cat-image.jpg). This is required.
Click ""Send MMS"".
Check the status message on the UI.
Check your terminal running the Next.js app for logs (console.log/console.error output).
Check the destination phone for the MMS message.
API Testing (Curl):
You can test the API endpoint directly using curl:
Replace placeholders with your actual test number, bucket, and media key. Check the JSON response in your terminal.
Unit/Integration Tests (Advanced):
Frontend: Use Jest and React Testing Library to test the MmsSender component's behavior (rendering, state changes, form submission). Mock the fetch call.
Backend: Use Jest to test the API route handler (POST function). Mock the @aws-sdk/client-pinpoint-sms-voice-v2 client using jest.mock to simulate successful responses and various error conditions from AWS without making actual API calls. This verifies your logic, validation, and error handling.
8. Deployment
Deploying a Next.js application is typically straightforward.
Choose a Platform: Vercel (creator of Next.js), Netlify, AWS Amplify, or containerizing with Docker and deploying to AWS ECS/EKS/App Runner are common choices.
Environment Variables: This is the most critical step for deployment.
Go to your chosen hosting provider's dashboard (e.g., Vercel Project Settings > Environment Variables).
Add the following environment variables with their production values:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY (Use IAM roles if deploying to AWS infrastructure like EC2/Lambda for better security)
AWS_REGION
SNS_ORIGINATION_NUMBER
S3_BUCKET_NAME
Ensure these variables are available to the ""Serverless Functions"" or backend environment.
Connect Git Repository: Link your GitHub/GitLab/Bitbucket repository to your hosting provider for automatic deployments on push.
Build & Deploy: The platform will typically build your Next.js application and deploy the static assets and API routes automatically.
Testing Production: Test the deployed application thoroughly, ensuring the environment variables are correctly configured and the application can communicate with AWS services. Check logs on your deployment platform.
9. Troubleshooting and Caveats
AccessDeniedException:
Cause: IAM user/role lacks required permissions (sms-voice:SendMediaMessage or s3:GetObject).
Solution: Verify the IAM policy attached to the user/role used by your application. Ensure the S3 resource ARN in the policy exactly matches your bucket (arn:aws:s3:::your-bucket-name/*).
ResourceNotFoundException:
Cause: The specified OriginationIdentity (phone number) doesn't exist or isn't configured correctly in the specified AWS_REGION, OR the MediaUrls S3 object doesn't exist or the bucket name is wrong.
Solution: Double-check the SNS_ORIGINATION_NUMBER value and the AWS_REGION. Verify the S3 URI (mediaUrl) is correct, the bucket exists in the same region, and the object key is accurate.
ValidationException:
Cause: Incorrectly formatted parameters (e.g., DestinationPhoneNumber not in E.164, MediaUrls not an array or invalid S3 URI format, message too long, invalid file type/size).
Solution: Review the parameters being sent in the SendMediaMessageCommandInput. Check phone number format, ensure MediaUrls is [s3Uri], and verify media file compliance with AWS limits. Check API logs for details.
Region Mismatch (CRITICAL):
Cause: The S3 bucket and the OriginationIdentity are in different AWS regions.
Solution: Ensure the S3 bucket, the origination number, and the AWS_REGION configured in your application/SDK client are all the SAME.
AWS SNS Sandbox Limits:
Cause: New AWS accounts are often in the SMS/MMS sandbox, limiting sending to verified phone numbers only.
Solution: Verify destination numbers in the AWS SNS console for testing, or request to move your account out of the sandbox via AWS Support for production use.
MMS Capability:
Cause: The OriginationIdentity (phone number) you acquired does not support MMS.
Solution: Verify the number's capabilities in the AWS console (Pinpoint/SNS). Acquire a number specifically enabled for MMS.
Frequently asked questions
You can send MMS messages by creating a Next.js API route that interacts with the AWS Pinpoint SMS and Voice V2 API. This API, along with the AWS SDK for JavaScript v3, allows you to send messages containing text and media hosted on Amazon S3.
The Amazon Pinpoint SMS and Voice V2 API is the AWS service used for sending MMS messages programmatically. While the AWS console might still reference SNS for message history, the Pinpoint API is what's used for sending MMS from your application.
Amazon S3 is used to store the media files (images, videos) included in MMS messages. It's chosen for its scalability, durability, and seamless integration with other AWS services, making it ideal for handling media in your MMS workflow.
The sms-voice namespace in the AWS SDK for JavaScript v3 is specifically for interacting with the Amazon Pinpoint SMS and Voice V2 API. Use this namespace when you need to send MMS messages or other SMS/voice communications programmatically.
While this guide uses Next.js for its ease of API route creation and full-stack capabilities, you can adapt the core logic to work with other React frameworks. You'll still need the AWS SDK for JavaScript v3 and interact with the Pinpoint API.
For local development, you can set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in a .env.local file. However, for production, it is best to use IAM roles for enhanced security. The AWS SDK will automatically use roles if available in your environment. Never commit credentials to version control.
Your IAM user or role needs the sms-voice:SendMediaMessage permission to send MMS via Pinpoint and s3:GetObject to allow the service to retrieve media from your designated S3 bucket.
Send a POST request to your API endpoint with a JSON body containing destinationPhoneNumber (in E.164 format), an optional messageBody, and mediaUrl which must be an S3 URI pointing to your media file.
The mediaUrl must be an S3 URI, for example, s3://your-bucket-name/image.jpg. It must follow this specific format so the Pinpoint API can retrieve the file.
Your S3 bucket, Origination Number, and the AWS region specified in your application/SDK client must all be the same. Mismatched regions will cause errors when trying to send MMS messages.
This error indicates missing IAM permissions. Verify your IAM user/role has sms-voice:SendMediaMessage and s3:GetObject permissions, ensuring the S3 resource ARN in the policy accurately specifies your bucket.
This could mean your origination phone number (SNS_ORIGINATION_NUMBER) is incorrect, doesn't exist in the specified AWS region, or the media file specified by the S3 URI (mediaUrl) is not found or the bucket is incorrect.
Check that the destinationPhoneNumber is in E.164 format, the mediaUrl is a valid S3 URI formatted as an array ([s3Uri]), and verify the media file meets AWS size and type limits. Ensure the origination number is also correct.
New AWS accounts are often in a sandbox, restricting sending to verified phone numbers. Verify the destination numbers in the AWS SNS console or request to be moved out of the sandbox via AWS Support for production use.
This guide provides a step-by-step walkthrough for building a feature within a Next.js application to send Multimedia Messaging Service (MMS) messages using AWS services. We'll leverage the AWS SDK for JavaScript v3, specifically interacting with the Amazon Pinpoint SMS and Voice v2 API (
sms-voicenamespace). While the AWS console might still reference SNS for SMS history, this newer Pinpoint API is used for sending MMS messages programmatically. Amazon S3 will be used for media storage.Project Overview and Goals
Goal: To create a simple Next.js application with a backend API endpoint capable of sending MMS messages containing text and an image hosted on Amazon S3 to a specified phone number.
Problem Solved: This provides a foundational implementation for applications needing to send rich media messages programmatically – useful for notifications, alerts, marketing campaigns, or user interactions involving images or other media.
Technologies:
@aws-sdk/client-pinpoint-sms-voice-v2). Chosen for its modularity and async/await support.SendMediaMessage) required for sending MMS messages.Architecture:
Prerequisites:
Note: You will set up the S3 bucket and upload the media file in Section 2 as part of this guide.
Outcome: By the end of this guide, you will have a functional Next.js application with a simple UI allowing users to input a destination phone number, a message body, and a media URL (hosted on S3), which triggers an API call to send an MMS message via AWS.
1. Setting up the Project
Let's initialize our Next.js project and install the necessary dependencies.
Create Next.js App: Open your terminal and run:
npx create-next-app@latest nextjs-aws-mms --typescript # or # yarn create next-app nextjs-aws-mms --typescriptFollow the prompts (App Router recommended: Yes, Tailwind CSS: Optional,
src/directory: Yes, Customize defaults: No).Navigate to Project Directory:
cd nextjs-aws-mmsInstall AWS SDK v3 Clients: We need the client for the Pinpoint SMS and Voice v2 API.
npm install @aws-sdk/client-pinpoint-sms-voice-v2 # or # yarn add @aws-sdk/client-pinpoint-sms-voice-v2Set Up Environment Variables: Create a file named
.env.localin the root of your project. Never commit this file to version control if it contains sensitive credentials..env.localis in your.gitignore.Configure IAM Permissions: The IAM user (or role) whose credentials your Next.js app uses needs permissions to:
Create an IAM policy (e.g.,
NextJsMmsAppPolicy) with the following JSON structure, replacing placeholders:{ ""Version"": ""2012-10-17"", ""Statement"": [ { ""Sid"": ""AllowMmsSend"", ""Effect"": ""Allow"", ""Action"": ""sms-voice:SendMediaMessage"", ""Resource"": ""*"" }, { ""Sid"": ""AllowS3Read"", ""Effect"": ""Allow"", ""Action"": ""s3:GetObject"", ""Resource"": ""arn:aws:s3:::YOUR_MMS_MEDIA_BUCKET_NAME/*"" } ] }YOUR_MMS_MEDIA_BUCKET_NAMEwith your actual bucket name.sms-voice:SendMediaMessageallows the application to use the specific API call for MMS.s3:GetObjectenables the Pinpoint service (acting on behalf of your call) to retrieve the media file from your bucket when constructing the MMS.2. AWS Resource Setup (S3 Bucket and Media)
MMS messages require media files to be hosted somewhere accessible. AWS S3 is the standard choice.
Create S3 Bucket:
S3_BUCKET_NAMEin.env.local)..env.localand where your Origination Number resides. This is critical for MMS sending.Upload Media File:
cat-image.jpg). Ensure it meets AWS MMS requirements (check Supported file types and sizes). Common formats like JPEG, PNG, GIF (under limits) are usually fine.Get Media File S3 URI:
s3://your-mms-media-bucket-name/cat-image.jpg). You will need this URI later for the API call. Note: This is not the public Object URL.3. Implementing the Backend API Route
We'll create a Next.js API route to handle the MMS sending logic.
Create API Route File: Create the file
src/app/api/send-mms/route.ts(if using App Router) orsrc/pages/api/send-mms.ts(if using Pages Router). This example uses the App Router structure.Implement the API Handler: Paste the following code into
src/app/api/send-mms/route.ts:// src/app/api/send-mms/route.ts import { NextRequest, NextResponse } from 'next/server'; import { PinpointSMSVoiceV2Client, SendMediaMessageCommand, SendMediaMessageCommandInput, } from '@aws-sdk/client-pinpoint-sms-voice-v2'; // Ensure environment variables are loaded (especially outside Vercel) // require('dotenv').config({ path: '.env.local' }); // Uncomment if needed locally and not using Next.js auto-loading // Retrieve environment variables (checks happen within the handler) const { AWS_REGION, SNS_ORIGINATION_NUMBER } = process.env; // Create the Pinpoint SMS Voice v2 Client // Credentials will be automatically sourced from environment variables // (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or IAM role if deployed on AWS infrastructure. const pinpointClient = new PinpointSMSVoiceV2Client({ region: AWS_REGION, // SDK will use AWS_REGION env var if not explicitly passed here }); export async function POST(request: NextRequest) { console.log('Received request to /api/send-mms'); // Validate required environment variables *per request* if (!AWS_REGION || !SNS_ORIGINATION_NUMBER) { console.error('Server configuration error: Missing AWS_REGION or SNS_ORIGINATION_NUMBER'); return NextResponse.json( { error: 'Server configuration error: Missing AWS environment variables.' }, { status: 500 } ); } let requestBody; try { requestBody = await request.json(); console.log('Request body:', requestBody); } catch (error) { console.error('Error parsing request body:', error); return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); } const { destinationPhoneNumber, messageBody, mediaUrl } = requestBody; // Basic Input Validation if (!destinationPhoneNumber || !mediaUrl) { return NextResponse.json( { error: 'Missing required fields: destinationPhoneNumber and mediaUrl are required.' }, { status: 400 } ); } // More robust validation could be added here (e.g., E.164 format check for phone number, S3 URI format check) if (!/^s3:\/\/[^\/]+\/.+/.test(mediaUrl)) { return NextResponse.json( { error: 'Invalid mediaUrl format. Must be an S3 URI (e.g., s3://bucket-name/key-name.jpg).' }, { status: 400 } ); } if (!/^\+\d{11,15}$/.test(destinationPhoneNumber)) { return NextResponse.json( { error: 'Invalid destinationPhoneNumber format. Must be E.164 format (e.g., +12223334444).' }, { status: 400 } ); } const params: SendMediaMessageCommandInput = { DestinationPhoneNumber: destinationPhoneNumber, // Provided in request OriginationIdentity: SNS_ORIGINATION_NUMBER, // From environment variable MessageBody: messageBody || '', // Optional message body from request MediaUrls: [mediaUrl], // S3 URI from request (must be an array) // ConfigurationSetName: 'YourOptionalConfigSetName' // If using configuration sets }; console.log('Attempting to send MMS with params:', params); try { const command = new SendMediaMessageCommand(params); const response = await pinpointClient.send(command); console.log('MMS sent successfully:', response); // The response contains the MessageId return NextResponse.json({ success: true, messageId: response.MessageId, message: 'MMS sent successfully', }, { status: 200 }); } catch (error: any) { console.error('Failed to send MMS:', error); // Provide more specific feedback if possible let errorMessage = 'Failed to send MMS.'; let statusCode = 500; if (error.name === 'ValidationException') { errorMessage = `Invalid request parameter: ${error.message}`; statusCode = 400; } else if (error.name === 'AccessDeniedException') { errorMessage = `Permission denied. Check IAM permissions for sms-voice:SendMediaMessage and s3:GetObject. ${error.message}`; statusCode = 403; } else if (error.name === 'ResourceNotFoundException') { errorMessage = `Resource not found (e.g., S3 object or Origination Number): ${error.message}`; statusCode = 404; } else if (error.name === 'ThrottlingException') { errorMessage = `Request throttled by AWS: ${error.message}`; statusCode = 429; } // Add more specific error handling as needed return NextResponse.json({ error: errorMessage, details: error.message }, { status: statusCode }); } }Code Explanation:
NextRequest,NextResponseand necessary AWS SDK v3 components.PinpointSMSVoiceV2Client. Credentials are automatically handled by the SDK based on environment variables or IAM roles.POSThandler function as required by Next.js App Router API routes.AWS_REGION,SNS_ORIGINATION_NUMBER) within the handler to ensure configuration is present for each request.destinationPhoneNumber,messageBody,mediaUrl).SendMediaMessageCommandInputobject using data from the request and environment variables. Note thatMediaUrlsmust be an array of S3 URIs.SendMediaMessageCommandwith the parameters.pinpointClient.send(command)within atry...catchblock to execute the API call.success: trueand themessageId.4. Implementing the Frontend UI
Now, let's create a simple React component to interact with our API endpoint.
Create Component File: Create
src/components/MmsSender.tsx.Implement the Component: Paste the following code:
// src/components/MmsSender.tsx 'use client'; // Required for components with interactivity (useState, onClick) in App Router import React, { useState } from 'react'; export default function MmsSender() { const [destinationPhoneNumber, setDestinationPhoneNumber] = useState(''); const [messageBody, setMessageBody] = useState(''); const [mediaUrl, setMediaUrl] = useState(''); // Should be an S3 URI entered by the user const [statusMessage, setStatusMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setIsLoading(true); setStatusMessage('Sending MMS...'); // Basic validation reminder if (!destinationPhoneNumber || !mediaUrl) { // Check if mediaUrl is provided setStatusMessage('Error: Destination Phone Number and Media URL (S3 URI) are required.'); setIsLoading(false); return; } // E.164 format validation reminder if (!/^\+\d{11,15}$/.test(destinationPhoneNumber)) { setStatusMessage('Error: Invalid phone number format. Use E.164 (e.g., +12223334444).'); setIsLoading(false); return; } // S3 URI format validation reminder (basic) if (!/^s3:\/\/[^\/]+\/.+/.test(mediaUrl)) { setStatusMessage('Error: Invalid Media URL format. Must be an S3 URI (e.g., s3://bucket-name/key-name.jpg).'); setIsLoading(false); return; } try { const response = await fetch('/api/send-mms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ destinationPhoneNumber, messageBody, mediaUrl: mediaUrl, // Use the S3 URI from state }), }); const result = await response.json(); if (response.ok && result.success) { setStatusMessage(`MMS sent successfully! Message ID: ${result.messageId}`); // Clear form on success setDestinationPhoneNumber(''); setMessageBody(''); setMediaUrl(''); } else { setStatusMessage(`Error: ${result.error || 'Failed to send MMS.'} ${result.details ? `(${result.details})` : ''}`); } } catch (error) { console.error('Network or unexpected error:', error); setStatusMessage('Error: Failed to connect to the API.'); } finally { setIsLoading(false); } }; return ( <div className=""max-w-md mx-auto mt-10 p-6 border rounded-lg shadow-lg""> <h2 className=""text-2xl font-semibold mb-4 text-center"">Send MMS via AWS</h2> <form onSubmit={handleSubmit}> <div className=""mb-4""> <label htmlFor=""destinationPhoneNumber"" className=""block text-sm font-medium text-gray-700 mb-1""> Destination Phone Number (E.164): </label> <input type=""tel"" id=""destinationPhoneNumber"" value={destinationPhoneNumber} onChange={(e) => setDestinationPhoneNumber(e.target.value)} placeholder=""+12223334444"" required className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"" /> </div> <div className=""mb-4""> <label htmlFor=""messageBody"" className=""block text-sm font-medium text-gray-700 mb-1""> Message Body (Optional): </label> <textarea id=""messageBody"" value={messageBody} onChange={(e) => setMessageBody(e.target.value)} rows={3} className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"" /> </div> <div className=""mb-4""> <label htmlFor=""mediaUrl"" className=""block text-sm font-medium text-gray-700 mb-1""> Media S3 URI: </label> <input type=""text"" id=""mediaUrl"" value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} placeholder=""s3://your-bucket/image.jpg"" required className=""w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"" /> <p className=""text-xs text-gray-500 mt-1"">Must be the S3 URI of the media file (e.g., s3://bucket-name/key).</p> </div> <button type=""submit"" disabled={isLoading} className=""w-full py-2 px-4 bg-indigo-600 text-white font-semibold rounded-md shadow hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"" > {isLoading ? 'Sending...' : 'Send MMS'} </button> </form> {statusMessage && ( <p className={`mt-4 text-sm ${statusMessage.startsWith('Error:') ? 'text-red-600' : 'text-green-600'}`}> {statusMessage} </p> )} </div> ); }useStateto manage form inputs and status messages. ThehandleSubmitfunction prevents default form submission, sets loading state, performs basic frontend validation checks (including for the S3 URI format), sends aPOSTrequest to/api/send-mmsusingfetch, includes the form data (using the user-providedmediaUrl) in the JSON body, and updates the status message based on the API response.Use the Component: Replace the content of
src/app/page.tsx(orsrc/pages/index.tsx) with:// src/app/page.tsx import MmsSender from '@/components/MmsSender'; // Adjust path if needed export default function HomePage() { return ( <main className=""flex min-h-screen flex-col items-center justify-center p-4""> <MmsSender /> </main> ); }5. Error Handling and Logging
Our API route includes basic error handling and logging:
try...catchblock aroundpinpointClient.send()catches errors from the AWS API.ValidationException,AccessDeniedException,ResourceNotFoundException,ThrottlingException) and return more informative messages and appropriate HTTP status codes (400, 403, 404, 429).500 Internal Server Erroris returned for unexpected issues.console.logandconsole.errorare used to log request details, success messages, and errors on the server side (visible in your terminal during local development or in your deployment provider's logs).Improvements:
pinoorwinstonfor structured JSON logging, which is easier to parse and analyze in production logging systems (e.g., CloudWatch Logs, Datadog).async-retry) for specific scenarios (e.g., temporary throttling if you anticipate bursts), although this adds complexity.6. Security Considerations
sms-voice:SendMediaMessage,s3:GetObjecton the specific bucket). Avoid usingAdministratorAccess.destinationPhoneNumber,messageBody,mediaUrl) to prevent injection attacks or malformed requests. Check formats (E.164, S3 URI), lengths, and potentially disallowed characters./api/send-mmsendpoint to prevent abuse and control costs. Libraries likerate-limiter-flexibleor platform features (Vercel, Cloudflare) can be used..env.local) to Git. Use environment variables provided by your hosting platform (Vercel, Netlify, AWS Systems Manager Parameter Store, Secrets Manager) for production deployments.7. Testing
Local Development:
npm run devoryarn dev.http://localhost:3000in your browser.+15551234567).s3://your-mms-media-bucket-name/cat-image.jpg). This is required.console.log/console.erroroutput).API Testing (Curl): You can test the API endpoint directly using
curl:curl -X POST http://localhost:3000/api/send-mms \ -H ""Content-Type: application/json"" \ -d '{ ""destinationPhoneNumber"": ""+1YOUR_TEST_PHONE_NUMBER"", ""messageBody"": ""Test message from curl!"", ""mediaUrl"": ""s3://YOUR_BUCKET_NAME/YOUR_MEDIA_KEY.jpg"" }'Replace placeholders with your actual test number, bucket, and media key. Check the JSON response in your terminal.
Unit/Integration Tests (Advanced):
MmsSendercomponent's behavior (rendering, state changes, form submission). Mock thefetchcall.POSTfunction). Mock the@aws-sdk/client-pinpoint-sms-voice-v2client usingjest.mockto simulate successful responses and various error conditions from AWS without making actual API calls. This verifies your logic, validation, and error handling.8. Deployment
Deploying a Next.js application is typically straightforward.
Choose a Platform: Vercel (creator of Next.js), Netlify, AWS Amplify, or containerizing with Docker and deploying to AWS ECS/EKS/App Runner are common choices.
Environment Variables: This is the most critical step for deployment.
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY(Use IAM roles if deploying to AWS infrastructure like EC2/Lambda for better security)AWS_REGIONSNS_ORIGINATION_NUMBERS3_BUCKET_NAMEConnect Git Repository: Link your GitHub/GitLab/Bitbucket repository to your hosting provider for automatic deployments on push.
Build & Deploy: The platform will typically build your Next.js application and deploy the static assets and API routes automatically.
Testing Production: Test the deployed application thoroughly, ensuring the environment variables are correctly configured and the application can communicate with AWS services. Check logs on your deployment platform.
9. Troubleshooting and Caveats
AccessDeniedException:sms-voice:SendMediaMessageors3:GetObject).arn:aws:s3:::your-bucket-name/*).ResourceNotFoundException:OriginationIdentity(phone number) doesn't exist or isn't configured correctly in the specifiedAWS_REGION, OR theMediaUrlsS3 object doesn't exist or the bucket name is wrong.SNS_ORIGINATION_NUMBERvalue and theAWS_REGION. Verify the S3 URI (mediaUrl) is correct, the bucket exists in the same region, and the object key is accurate.ValidationException:DestinationPhoneNumbernot in E.164,MediaUrlsnot an array or invalid S3 URI format, message too long, invalid file type/size).SendMediaMessageCommandInput. Check phone number format, ensureMediaUrlsis[s3Uri], and verify media file compliance with AWS limits. Check API logs for details.OriginationIdentityare in different AWS regions.AWS_REGIONconfigured in your application/SDK client are all the SAME.OriginationIdentity(phone number) you acquired does not support MMS.