code examples
code examples
How to Send MMS with Plivo Node.js SDK: Complete Next.js + Supabase Guide
Learn to send MMS messages using Plivo's Node.js SDK in Next.js with Supabase integration. Step-by-step tutorial with code examples, webhooks, error handling, and production deployment best practices.
Send MMS with Plivo Node.js SDK: Complete Next.js + Supabase Tutorial
Learn how to send MMS messages using Plivo's Node.js SDK with Next.js and Supabase. Multimedia Messaging Service (MMS) enables you to send rich media content like images, GIFs, and videos alongside text messages. This comprehensive tutorial guides you through building a production-ready Node.js application that sends MMS messages via the Plivo API while tracking delivery status in Supabase.
When to Send MMS vs. SMS Messages
| Use MMS For | Use SMS For |
|---|---|
| Marketing campaigns with rich media | Transactional messages |
| Product showcases and demonstrations | Appointment reminders |
| Event invitations with visual elements | Quick alerts and OTPs |
| Interactive customer engagement | Time-sensitive text notifications |
MMS achieves higher engagement rates due to multimedia content but costs more per message than SMS. Source: SMS vs MMS Marketing Comparison 2025
This tutorial shows you how to build a Next.js API route that accepts recipient details, message text, and media URLs, then uses the Plivo Node.js SDK to send MMS messages programmatically. You'll also learn to track message delivery status in Supabase for monitoring and historical records.
What You'll Learn:
- Integrate Plivo's messaging API into Next.js
- Handle multimedia content delivery
- Implement webhook callbacks for delivery status tracking
- Store message logs in Supabase with Row Level Security
- Apply production-ready error handling and retry logic
System Architecture:
graph LR
A[User/Client] -- HTTP POST Request --> B(Next.js API Route);
B -- Send MMS Request --> C(Plivo API);
B -- Store Message Log --> D(Supabase Database);
C -- Delivers MMS --> E(Carrier Network);
E -- Delivers MMS --> F(Recipient Phone);
C -- Sends Status Callback --> B;
B -- Update Status --> D;Prerequisites:
- Node.js and npm (or yarn): Node.js version 18.18 or later for Next.js 15. Download from https://nodejs.org/. Source: Next.js 15 System Requirements
- Plivo Account: Sign up at https://www.plivo.com/.
- Plivo Phone Number: An MMS-enabled phone number (available for US and Canadian numbers). Acquire one from the Plivo console under "Phone Numbers" > "Buy Numbers".
- Supabase Account: Free account for database hosting. Sign up at https://supabase.com/.
- Basic knowledge of Node.js, Next.js, Supabase, and REST APIs.
1. Setting Up Your Next.js Project to Send MMS with Plivo
Initialize your Next.js project and install the necessary dependencies for sending MMS with Plivo and storing data in Supabase.
-
Create Next.js Project: Open your terminal and create a new Next.js application with TypeScript support.
bashnpx create-next-app@latest plivo-mms-sender cd plivo-mms-senderWhen prompted, select:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Optional (your preference)
src/directory: Yes- App Router: Yes
- Customize default import alias: No
-
Install Dependencies: Install
plivofor the Plivo Node.js SDK (version 4.74.0 as of January 2025) and@supabase/supabase-jsfor the Supabase client. The Plivo SDK requires Node.js 5.5 or higher and is fully compatible with Node.js 18.18+ used by Next.js 15. Sources: Plivo Node.js SDK Documentation; Plivo SDK Version Compatibilitybashnpm install plivo @supabase/supabase-js -
Set Up Environment Variables: Create
.env.localin your project root to store sensitive credentials. Never commit this file to version control.Filename:
.env.localdotenvPLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_SENDER_NUMBER=YOUR_PLIVO_MMS_ENABLED_NUMBER NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_PROJECT_URL NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEYPLIVO_AUTH_ID&PLIVO_AUTH_TOKEN: Find these on your Plivo console dashboard overview page.PLIVO_SENDER_NUMBER: Your MMS-enabled Plivo phone number with country code (e.g.,+14151234567).- Supabase credentials: Find these in your Supabase project settings under "API".
-
Update
.gitignoreFile: Next.js automatically includes.env.localin.gitignore, ensuring your credentials aren't tracked by Git. -
Basic Project Structure: Your Next.js project structure should look like:
plivo-mms-sender/ ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ └── send-mms/ │ │ │ └── route.ts │ │ └── layout.tsx │ └── lib/ │ ├── plivo.ts │ └── supabase.ts ├── .env.local ├── .gitignore ├── package.json ├── package-lock.json └── next.config.js
2. Creating Supabase Database Schema for MMS Tracking
Create a Supabase table to track MMS message logs with proper Row Level Security policies.
-
Create Messages Table: In your Supabase dashboard, navigate to the SQL Editor and run this SQL:
sqlCREATE TABLE message_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), plivo_message_uuid TEXT UNIQUE, sender TEXT NOT NULL, recipient TEXT NOT NULL, message_text TEXT, media_urls TEXT[], status TEXT DEFAULT 'queued', plivo_status_info JSONB, error_code TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Index for efficient lookups CREATE INDEX idx_plivo_message_uuid ON message_logs(plivo_message_uuid); CREATE INDEX idx_status ON message_logs(status); CREATE INDEX idx_created_at ON message_logs(created_at DESC); -- Enable Row Level Security ALTER TABLE message_logs ENABLE ROW LEVEL SECURITY; -- Policy to allow service role full access CREATE POLICY "Service role has full access to message_logs" ON message_logs FOR ALL TO service_role USING (true) WITH CHECK (true); -- Optional: Policy for authenticated users to read their own messages -- (Modify based on your authentication structure) CREATE POLICY "Users can read message_logs" ON message_logs FOR SELECT TO authenticated USING (true); -- Function to update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to automatically update updated_at CREATE TRIGGER update_message_logs_updated_at BEFORE UPDATE ON message_logs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -
Verify Table Creation: Navigate to the Table Editor in Supabase and confirm the
message_logstable exists with proper columns and indexes.
3. Initializing Plivo and Supabase Clients in Node.js
Create utility modules for initializing Supabase and Plivo clients.
src/lib/supabase.ts:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error('Missing Supabase environment variables');
}
// Use service role key for server-side operations that bypass RLS
export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
// Database types
export interface MessageLog {
id: string;
plivo_message_uuid: string | null;
sender: string;
recipient: string;
message_text: string | null;
media_urls: string[];
status: string;
plivo_status_info: any;
error_code: string | null;
created_at: string;
updated_at: string;
}src/lib/plivo.ts:
import plivo from 'plivo';
const PLIVO_AUTH_ID = process.env.PLIVO_AUTH_ID;
const PLIVO_AUTH_TOKEN = process.env.PLIVO_AUTH_TOKEN;
const PLIVO_SENDER_NUMBER = process.env.PLIVO_SENDER_NUMBER;
if (!PLIVO_AUTH_ID || !PLIVO_AUTH_TOKEN || !PLIVO_SENDER_NUMBER) {
throw new Error('Missing Plivo environment variables');
}
// Initialize Plivo client once and export
export const plivoClient = new plivo.Client(PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN);
export const senderNumber = PLIVO_SENDER_NUMBER;4. Building the Next.js API Route to Send MMS via Plivo
Create the API endpoint that receives requests to send MMS messages via Plivo and logs them to Supabase.
src/app/api/send-mms/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { plivoClient, senderNumber } from '@/lib/plivo';
import { supabaseAdmin } from '@/lib/supabase';
// Enable CORS for client-side requests
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { destinationNumber, messageText, mediaUrl } = body;
// --- Basic Request Validation ---
if (!destinationNumber || !messageText || !mediaUrl) {
return NextResponse.json(
{ error: 'Missing required fields: destinationNumber, messageText, mediaUrl' },
{ status: 400 }
);
}
// E.164 format validation
if (!/^\+\d{10,15}$/.test(destinationNumber)) {
return NextResponse.json(
{ error: 'Invalid destinationNumber format. Use E.164 format (e.g., +14157654321).' },
{ status: 400 }
);
}
// URL validation
try {
new URL(mediaUrl);
} catch (_) {
return NextResponse.json(
{ error: 'Invalid mediaUrl format.' },
{ status: 400 }
);
}
// Message text length validation (1,600 character limit for MMS)
if (messageText.length > 1600) {
return NextResponse.json(
{ error: 'Message text exceeds 1,600 character limit for MMS.' },
{ status: 400 }
);
}
// Create initial message log in Supabase
const { data: messageLog, error: dbError } = await supabaseAdmin
.from('message_logs')
.insert({
sender: senderNumber,
recipient: destinationNumber,
message_text: messageText,
media_urls: [mediaUrl],
status: 'queued'
})
.select()
.single();
if (dbError) {
console.error('Database error:', dbError);
return NextResponse.json(
{ error: 'Failed to create message log', details: dbError.message },
{ status: 500 }
);
}
// Send MMS via Plivo
try {
const response = await plivoClient.messages.create(
senderNumber,
destinationNumber,
messageText,
{
type: 'mms',
media_urls: [mediaUrl],
// Optional: Configure callback URL for status updates
// url: `${process.env.NEXT_PUBLIC_APP_URL}/api/plivo-callback`
}
);
// Update message log with Plivo UUID
await supabaseAdmin
.from('message_logs')
.update({
plivo_message_uuid: response.messageUuid[0],
status: 'sent'
})
.eq('id', messageLog.id);
return NextResponse.json({
message: 'MMS sent successfully',
messageId: messageLog.id,
plivoMessageUuid: response.messageUuid[0]
}, { status: 202 });
} catch (plivoError: any) {
console.error('Plivo error:', plivoError);
// Update message log with error
await supabaseAdmin
.from('message_logs')
.update({
status: 'failed',
error_code: plivoError.statusCode?.toString(),
plivo_status_info: { error: plivoError.message }
})
.eq('id', messageLog.id);
let statusCode = 500;
let errorMessage = 'Failed to send MMS';
if (plivoError.statusCode) {
switch (plivoError.statusCode) {
case 400:
statusCode = 400;
errorMessage = plivoError.message || 'Bad Request (invalid number or media URL)';
break;
case 401:
statusCode = 401;
errorMessage = 'Authentication failed';
break;
case 500:
case 503:
statusCode = 503;
errorMessage = 'Plivo service unavailable';
break;
default:
if (plivoError.statusCode >= 400 && plivoError.statusCode < 600) {
statusCode = plivoError.statusCode;
errorMessage = plivoError.message || `Plivo API error (${plivoError.statusCode})`;
}
}
}
return NextResponse.json(
{ error: errorMessage, details: plivoError.message },
{ status: statusCode }
);
}
} catch (error: any) {
console.error('Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ status: 500 }
);
}
}Key Features:
- CORS Support: The
OPTIONShandler enables Cross-Origin Resource Sharing for browser requests - Validation: Validates phone number format (E.164), media URL, and enforces 1,600 character limit for MMS. Source: Plivo MMS Size Limitations
- Database Logging: Creates message log before sending, then updates with Plivo UUID or error details
- Error Handling: Returns appropriate HTTP status codes for all error conditions
5. Implementing Plivo Webhook Handler for MMS Delivery Status
Create an API route to receive delivery status callbacks from Plivo and update Supabase.
src/app/api/plivo-callback/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
import crypto from 'crypto';
// Validate Plivo webhook signature
function validatePlivoSignature(
url: string,
nonce: string,
signature: string,
authToken: string
): boolean {
const message = url + nonce;
const expectedSignature = crypto
.createHmac('sha256', authToken)
.update(message)
.digest('base64');
return signature === expectedSignature;
}
export async function POST(request: NextRequest) {
try {
// Get signature headers
const signature = request.headers.get('X-Plivo-Signature-V2');
const nonce = request.headers.get('X-Plivo-Signature-V2-Nonce');
if (!signature || !nonce) {
return NextResponse.json(
{ error: 'Missing signature headers' },
{ status: 401 }
);
}
// Validate signature
const url = request.url;
const authToken = process.env.PLIVO_AUTH_TOKEN!;
if (!validatePlivoSignature(url, nonce, signature, authToken)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse callback data
const body = await request.json();
const { MessageUUID, Status, ErrorCode } = body;
if (!MessageUUID) {
return NextResponse.json(
{ error: 'Missing MessageUUID' },
{ status: 400 }
);
}
// Update message log in Supabase
const { error } = await supabaseAdmin
.from('message_logs')
.update({
status: Status?.toLowerCase() || 'unknown',
error_code: ErrorCode || null,
plivo_status_info: body
})
.eq('plivo_message_uuid', MessageUUID);
if (error) {
console.error('Failed to update message log:', error);
return NextResponse.json(
{ error: 'Failed to update message log' },
{ status: 500 }
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error: any) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Webhook Security: This implementation validates Plivo webhook signatures using V2 signature method with HMAC-SHA256 and nonce validation. For SMS/MMS webhooks, Plivo uses X-Plivo-Signature-V2 and X-Plivo-Signature-V2-Nonce headers. Calculate the signature by concatenating the full callback URL with the nonce value, then sign with HMAC-SHA256 using your Auth Token. Sources: Plivo Webhook Signature Validation; Plivo V3 Signature Documentation
6. Testing Your Plivo MMS API Endpoint
Test your API endpoint using curl or Postman.
Using curl:
curl -X POST http://localhost:3000/api/send-mms \
-H "Content-Type: application/json" \
-d '{
"destinationNumber": "+14157654321",
"messageText": "Hello from Next.js! Check out this image.",
"mediaUrl": "https://media.giphy.com/media/26gscSULUcfKU7dHq/source.gif"
}'Success Response (Status 202):
{
"message": "MMS sent successfully",
"messageId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"plivoMessageUuid": "x1y2z3w4-v5u6-t7s8-r9q0-p1o2n3m4l5k6"
}Verify in Supabase:
Check your message_logs table to confirm the message was logged with status "sent".
7. Production-Ready Error Handling and Retry Logic for MMS
Production applications require robust error handling strategies.
-
Structured Logging: Replace
console.logandconsole.errorwith Pino or Winston. -
Retry Mechanisms: Implement retry logic for transient errors (5xx status codes) using
async-retry. Only retry network errors or Plivo 5xx responses – never 4xx client errors. -
Validation Enhancement: Use
libphonenumber-jsfor phone number validation andzodfor request schema validation. -
Media File Validation: Validate media file size and type before sending to Plivo using a HEAD request:
typescriptasync function validateMediaFile(url: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch(url, { method: 'HEAD' }); const contentLength = parseInt(response.headers.get('content-length') || '0'); const contentType = response.headers.get('content-type'); // Individual file limit: 2 MB if (contentLength > 2 * 1024 * 1024) { return { valid: false, error: 'File exceeds 2 MB limit' }; } // Check supported MIME types const supportedTypes = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4']; if (contentType && !supportedTypes.includes(contentType)) { return { valid: false, error: 'Unsupported file type' }; } return { valid: true }; } catch (error) { return { valid: false, error: 'Unable to validate media file' }; } }
8. Understanding Plivo MMS Limitations and Media Requirements
Understand media files and carrier-specific requirements for reliable MMS delivery.
-
Supported Countries: Plivo supports sending MMS via long codes to the US and Canada only. Messages to other countries may fail or fall back to SMS without media. Source: Plivo MMS Documentation, 2025.
-
Media File Types and Size Limits:
Limit Type Sending Receiving Total size 5 MB 7 MB Per attachment 2 MB 2 MB Max attachments 10 10 Message text 1,600 characters 1,600 characters Supported Formats: JPEG, PNG, GIF, and some video/audio formats.
Carrier-Specific Recommendations:
- US carriers: Keep non-JPEG/PNG/GIF files under 600 KB for best compatibility
- Canadian carriers: Keep all attachments under 1 MB
- Best Practice: Aim for individual files under 600 KB and total message size under 1 MB for optimal delivery.
Sources: Plivo MMS Size Limits Support Article; Plivo Send Message API Documentation
-
Media URL Accessibility: The
mediaUrlmust be publicly accessible without authentication. Plivo servers fetch this URL to attach media. Private or localhost URLs will fail. -
Multiple Media Files: Send up to 10 images/GIFs in one MMS by providing an array of URLs in
media_urls. Keep total size under 5 MB. -
Fallback to SMS: Implement fallback logic to send SMS if MMS fails (e.g., non-MMS-capable number, destination outside US/Canada, or specific error codes).
9. Security Best Practices for Production MMS APIs
Protect your MMS application with these essential security measures.
-
Secure Credentials: Use
.env.localfor development and Vercel Environment Variables or AWS Secrets Manager for production. Never commit secrets to version control. -
Input Validation and Sanitization:
- Use
libphonenumber-jsfor E.164 validation - Use
zodfor request body schema validation - Sanitize user-provided text to prevent XSS
- Use
-
Rate Limiting: Implement rate limiting using Next.js middleware or edge functions:
typescript// src/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const rateLimit = new Map<string, { count: number; resetTime: number }>(); export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/api/send-mms')) { const ip = request.ip || 'unknown'; const now = Date.now(); const limit = 10; // requests const window = 60 * 1000; // 1 minute const record = rateLimit.get(ip); if (record) { if (now < record.resetTime) { if (record.count >= limit) { return NextResponse.json( { error: 'Too many requests' }, { status: 429 } ); } record.count++; } else { rateLimit.set(ip, { count: 1, resetTime: now + window }); } } else { rateLimit.set(ip, { count: 1, resetTime: now + window }); } } return NextResponse.next(); } -
CORS Configuration: The API route includes CORS headers in the
OPTIONShandler. In production, replace*with specific domains inAccess-Control-Allow-Origin. -
Authentication/Authorization: For production APIs, implement JWT-based authentication using NextAuth.js or Supabase Auth:
typescriptimport { createServerClient } from '@supabase/ssr'; export async function POST(request: NextRequest) { const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get: (name) => request.cookies.get(name)?.value, }, } ); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Proceed with MMS sending... } -
Webhook Security: Always validate Plivo webhook signatures using HMAC-SHA256 with V2 headers before processing data.
10. Deploying Your Plivo MMS Application to Production
Deploy your MMS application to production:
-
Vercel Deployment (Recommended for Next.js):
- Push your code to GitHub/GitLab
- Connect repository to Vercel
- Add environment variables in Vercel dashboard
- Deploy with automatic HTTPS and edge functions
-
Environment Variables:
- Set all variables from
.env.localin your hosting platform - Use separate Plivo accounts for staging and production
- Rotate credentials regularly
- Set all variables from
-
Supabase Configuration:
- Enable database backups
- Configure RLS policies for production security
- Set up connection pooling for high traffic
- Monitor query performance
-
Webhook URL Configuration:
- Update Plivo callback URL to your production domain
- Ensure webhook endpoint is publicly accessible
- Configure in Plivo console or pass as
urlparameter when sending
-
Monitoring and Logging:
- Implement error tracking (Sentry, LogRocket)
- Set up Supabase monitoring and alerts
- Monitor Plivo usage and costs in console
- Track message delivery rates
Frequently Asked Questions (FAQ)
How do I send MMS messages using Plivo Node.js SDK in Next.js?
To send MMS with Plivo in Node.js, use the Plivo SDK's client.messages.create() method with type: 'mms' and media_urls parameters. First, install the SDK with npm install plivo, then initialize the client with your Auth ID and Token. Create a Next.js API route that calls this method with your sender number, recipient number, message text, and media URLs to send MMS programmatically.
Which countries support MMS messaging with Plivo?
Plivo supports MMS in the United States and Canada only. MMS to other countries may fail or fall back to SMS without media.
What are Plivo's MMS file size and attachment limits?
Plivo MMS limits: 5 MB total for sending, 7 MB total for receiving, 2 MB per attachment, up to 10 attachments per message, and 1,600 characters for message text. Keep individual files under 600 KB for best delivery.
How do I enable CORS for Next.js API routes when sending MMS?
Add an OPTIONS handler that returns CORS headers. Set Access-Control-Allow-Origin to your frontend domains, specify allowed methods (POST, OPTIONS), and include headers (Content-Type, Authorization).
How do I validate Plivo webhook signatures for MMS delivery status?
Plivo uses V2 signatures for SMS/MMS webhooks with X-Plivo-Signature-V2 and X-Plivo-Signature-V2-Nonce headers. Concatenate your callback URL with the nonce, then sign with HMAC-SHA256 using your Auth Token. Compare the result with the signature header to validate authenticity.
How do I store MMS logs in Supabase with Row Level Security?
Create a table with RLS enabled and appropriate policies. Use the Supabase service role key for server-side operations that need to bypass RLS. Create policies that allow the service role full access while restricting client access based on your authentication requirements.
Why is my Plivo MMS message failing or not being delivered?
Common issues: invalid or non-MMS-enabled sender number, media URL not publicly accessible, file size exceeding limits, unsupported format, destination outside US/Canada, or insufficient balance. Check Plivo Debug Logs for details.
Can I send multiple images or media files in one MMS message?
Yes. Send up to 10 media attachments in one MMS by providing an array of URLs in media_urls. Keep total size under 5 MB.
What Node.js version is required for Next.js 15?
Next.js 15 requires Node.js 18.18 or later. The Plivo Node.js SDK is compatible with Node.js 5.5 and higher, making it fully compatible with Next.js 15 requirements.
How do I deploy a Next.js MMS application with Plivo to Vercel?
Push your code to a Git repository, connect it to Vercel, configure environment variables in the Vercel dashboard (including Plivo and Supabase credentials), and deploy. Vercel automatically handles HTTPS, edge functions, and provides webhook URLs for Plivo callbacks.