code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Send MMS with Plivo, Next.js 14, and NextAuth: Complete Tutorial

Learn how to send MMS messages using Plivo API in Next.js 14 with NextAuth authentication. Step-by-step guide with TypeScript, secure API endpoints, and error handling.

Send MMS with Plivo, Next.js, and NextAuth

Build a feature in your Next.js application that enables authenticated users to send Multimedia Messaging Service (MMS) messages using the Plivo Communications Platform.

You'll leverage NextAuth.js for robust user authentication, ensuring only logged-in users can trigger MMS sending via a secure API endpoint. This approach solves the common need to integrate communication features directly into web applications while maintaining security and leveraging a reliable third-party service for message delivery.

Goal: Create a secure, server-side API endpoint in Next.js protected by NextAuth. This endpoint will accept recipient details and a media URL, then use the Plivo Node.js SDK to send an MMS message. Build a simple frontend interface for authenticated users to utilize this functionality.

Technologies Used:

  • Next.js (v14+ with App Router): A React framework for building server-rendered and static web applications. Choose this for its robust features, performance optimizations, and integrated API routes.
  • NextAuth.js (v5+): A complete authentication solution for Next.js applications. Use this to handle user sessions, login providers, and route protection seamlessly.
  • Plivo: A cloud communications platform providing APIs for SMS, MMS, Voice, and more. Select this for reliable messaging services and a developer-friendly Node.js SDK.
  • Node.js (v18+): The JavaScript runtime environment.
  • TypeScript: For type safety and improved developer experience.
  • Tailwind CSS: For styling the simple user interface.

(Note: This guide targets Next.js 14+, NextAuth v5+, and Node.js v18+. While accurate at the time of writing, check the official documentation for these libraries to ensure compatibility with the latest versions or confirm these remain the target versions for your project.)

System Architecture:

(Note: A visual diagram illustrating the flow would typically be placed here. The flow is: User interacts with Next.js Frontend → Frontend sends data to Next.js Server → Server calls /api/send-mms → API Route verifies NextAuth session → If valid, initializes Plivo Client → Sends MMS via Plivo API → Plivo API delivers to Recipient. Responses flow back accordingly.)

Prerequisites:

  • Install Node.js (v18 or later).
  • Install npm or yarn package manager.
  • Create a Plivo account (Sign up here).
  • Understand React, Next.js, and asynchronous JavaScript basics.
  • Purchase or verify an MMS-enabled phone number within your Plivo account (for US/Canada destinations).
  • Obtain a publicly accessible URL for the media file you want to send (e.g., hosted on S3, Cloudinary, or a public server). Plivo also allows uploading media directly via their console or API.

MMS Geographic Limitations:

  • US and Canada Only: Plivo supports sending and receiving MMS messages exclusively within the United States and Canada (Plivo MMS Support). International MMS messaging outside these two countries is not supported as of November 2024.
  • Number Requirements: You must use an MMS-enabled Plivo long-code phone number. Only US and Canadian local long-code numbers support MMS functionality.
  • Carrier Support: MMS is supported by major mobile carriers in the US and Canada. Plivo phone numbers cannot receive MMS from short code numbers.

Final Outcome:

By the end of this guide, you will have:

  1. A Next.js application with basic email/password authentication using NextAuth.js.
  2. A secure API endpoint (/api/send-mms) that uses the Plivo SDK to send MMS messages.
  3. A protected frontend page where authenticated users can input a recipient number, message text, and media URL to send an MMS.
  4. Proper environment variable management for API keys and secrets.
  5. Basic error handling for the MMS sending process.
  6. TypeScript type augmentation for custom NextAuth session properties.

1. Set Up Your Project

Initialize your Next.js project and install the necessary dependencies.

  1. Create Next.js App: Open your terminal and run this command to create a new Next.js project using the App Router and TypeScript:

    bash
    npx create-next-app@latest nextjs-plivo-mms --typescript --tailwind --eslint --app --src-dir --import-alias '@/*'

    Follow the prompts and choose your preferred settings.

  2. Navigate to Project Directory:

    bash
    cd nextjs-plivo-mms
  3. Install Dependencies: Install next-auth for authentication and plivo for the Plivo API interaction. Add bcrypt for password hashing if using the Credentials provider and its types.

    bash
    npm install next-auth plivo bcrypt
    npm install -D @types/bcrypt

    (Alternatively, use yarn add next-auth plivo bcrypt and yarn add -D @types/bcrypt)

  4. Initialize Prisma (Optional but Recommended for User Management): While not strictly required just to send an MMS via Plivo triggered by an authenticated user, integrate a database to manage user accounts if you use the Credentials provider or want to store user-specific data. Use Prisma with PostgreSQL as an example.

    bash
    npm install @prisma/client @next-auth/prisma-adapter
    npm install -D prisma
    npx prisma init --datasource-provider postgresql

    This creates a prisma directory with a schema.prisma file and a .env file for the database connection string.

    Alternative Database Options: Besides PostgreSQL, Prisma supports MySQL, SQLite, MongoDB, SQL Server, and CockroachDB. For serverless deployments, consider Neon, PlanetScale, or Supabase PostgreSQL with connection pooling.

  5. Configure Environment Variables: NextAuth requires a secret key, and Plivo needs API credentials. Create a .env.local file in your project root. Never commit this file to version control. Add .env.local to your .gitignore file if it's not already there.

    • Generate NextAuth Secret:

      bash
      openssl rand -base64 32

      Copy the output.

    • Edit .env.local:

      plaintext
      # .env.local
      
      # NextAuth Configuration
      # Generate with: openssl rand -base64 32
      NEXTAUTH_SECRET=YOUR_GENERATED_NEXTAUTH_SECRET
      # Must be the canonical URL of your deployment. Crucial for redirects, callbacks, etc.
      # For local development:
      NEXTAUTH_URL=http://localhost:3000
      # For production (replace with your actual domain):
      # NEXTAUTH_URL=https://your-app-domain.com
      
      # Plivo Credentials
      # Find these in your Plivo Console: https://console.plivo.com/dashboard/
      PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
      PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
      
      # Plivo Phone Number
      # An MMS-enabled number from your Plivo account (e.g., +14151234567)
      # Must be in E.164 format (includes + and country code)
      PLIVO_SENDER_NUMBER=YOUR_PLIVO_MMS_ENABLED_NUMBER
      
      # Prisma Database Connection (if using Prisma)
      # Example for local PostgreSQL: postgresql://user:password@localhost:5432/mydatabase
      # Example for Neon/Supabase: Get from your provider dashboard
      DATABASE_URL="YOUR_DATABASE_CONNECTION_STRING"
    • Get Plivo Credentials:

      1. Log in to your Plivo Console.
      2. Find your AUTH ID and AUTH TOKEN displayed prominently on the main Dashboard page in the "Account Info" section. Click the eye icon to reveal the token.
      3. Navigate to "Phone Numbers" → "Your Numbers". Ensure you have a number listed with MMS capability enabled (check the "Capabilities" column). If not, go to "Buy Numbers" to purchase one (filter by MMS capability for US/Canada). Copy the full number in E.164 format (e.g., +12025550111) into PLIVO_SENDER_NUMBER.

    Plivo Pricing and Trial Accounts:

    • Trial Credits: New Plivo accounts receive trial credits (typically $25 USD) for testing purposes. Trial accounts can only send messages to verified phone numbers that you manually add in the Plivo Console under "Sandbox Numbers" or "Phone Numbers" → "Sandbox".
    • US MMS Pricing: MMS messages within the US typically cost $0.01-$0.015 per message segment (as of 2024). Phone number rental for US local long-code MMS-enabled numbers costs approximately $0.50/month (Plivo US Pricing).
    • Production Use: For production deployment, you'll need to add funds to your account and complete any required compliance registrations (such as 10DLC for US A2P messaging).
  6. Configure Prisma Schema (if using Prisma): Update prisma/schema.prisma to include the necessary models for NextAuth integration.

    prisma
    // prisma/schema.prisma
    
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    model Account {
      id                String  @id @default(cuid())
      userId            String
      type              String
      provider          String
      providerAccountId String
      refresh_token     String? @db.Text
      access_token      String? @db.Text
      expires_at        Int?
      token_type        String?
      scope             String?
      id_token          String? @db.Text
      session_state     String?
    
      user User @relation(fields: [userId], references: [id], onDelete: Cascade)
    
      @@unique([provider, providerAccountId])
    }
    
    model Session {
      id           String   @id @default(cuid())
      sessionToken String   @unique
      userId       String
      expires      DateTime
      user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
    }
    
    model User {
      id            String    @id @default(cuid())
      name          String?
      email         String?   @unique
      emailVerified DateTime?
      password      String?   // Added for Credentials provider
      image         String?
      accounts      Account[]
      sessions      Session[]
    }
    
    model VerificationToken {
      identifier String
      token      String   @unique
      expires    DateTime
    
      @@unique([identifier, token])
    }
  7. Apply Prisma Migrations (if using Prisma): Run the migration command to create database tables based on your schema.

    bash
    npx prisma migrate dev --name init_nextauth

    This also generates the Prisma Client.

2. Implement Core Functionality (NextAuth & Plivo Client)

Set up NextAuth for authentication and configure the Plivo client.

  1. Configure NextAuth: Create the NextAuth API route handler.

    • Create Prisma Client Instance (if using Prisma): Create a utility file to instantiate Prisma Client, preventing multiple instances in development.

      typescript
      // src/lib/prisma.ts
      import { PrismaClient } from '@prisma/client';
      
      declare global {
        // allow global `var` declarations
        // eslint-disable-next-line no-var
        var prisma: PrismaClient | undefined;
      }
      
      export const prisma =
        global.prisma ||
        new PrismaClient({
          log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
        });
      
      if (process.env.NODE_ENV !== 'production') global.prisma = prisma;
    • Create NextAuth Type Definitions: TypeScript module augmentation is required to add custom properties to NextAuth session and token objects. Create a type definition file:

      typescript
      // src/types/next-auth.d.ts
      import NextAuth, { DefaultSession } from "next-auth";
      import { JWT } from "next-auth/jwt";
      
      declare module "next-auth" {
        /**
         * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
         */
        interface Session {
          user: {
            /** The user's unique ID from the database */
            id: string;
          } & DefaultSession["user"]
        }
      
        interface User {
          id: string;
          email?: string | null;
          name?: string | null;
          image?: string | null;
        }
      }
      
      declare module "next-auth/jwt" {
        /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
        interface JWT {
          /** The user's unique ID */
          id: string;
          email?: string | null;
        }
      }

      Ensure your tsconfig.json includes this file in its typeRoots or include paths. The Next.js App Router typically auto-discovers .d.ts files in the src directory.

    • Create NextAuth Options: It's good practice to define auth options separately.

      typescript
      // src/lib/authOptions.ts
      import { AuthOptions } from 'next-auth';
      import CredentialsProvider from 'next-auth/providers/credentials';
      import { PrismaAdapter } from '@next-auth/prisma-adapter';
      import { prisma } from '@/lib/prisma'; // Adjust path if needed
      import bcrypt from 'bcrypt';
      
      export const authOptions: AuthOptions = {
        // Use Prisma Adapter to store users, sessions, etc. in the DB
        adapter: PrismaAdapter(prisma),
        providers: [
          // Example: Credentials Provider (Email/Password)
          CredentialsProvider({
            name: 'Credentials',
            credentials: {
              email: { label: 'Email', type: 'email', placeholder: 'user@example.com' },
              password: { label: 'Password', type: 'password' },
            },
            async authorize(credentials) {
              if (!credentials?.email || !credentials?.password) {
                console.error('Missing credentials');
                return null;
              }
      
              const user = await prisma.user.findUnique({
                where: { email: credentials.email },
              });
      
              if (!user || !user.password) {
                console.error('No user found or password not set for:', credentials.email);
                return null; // Don't reveal if user exists
              }
      
              const isValidPassword = await bcrypt.compare(
                credentials.password,
                user.password
              );
      
              if (!isValidPassword) {
                console.error('Invalid password for:', credentials.email);
                return null;
              }
      
              console.log('User authorized:', user.email);
              // Return user object without password
              return { id: user.id, name: user.name, email: user.email, image: user.image };
            },
          }),
          // Add other providers like Google, GitHub, etc. here
          // Example: GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! })
        ],
        // Use JWT strategy for session management
        session: {
          strategy: 'jwt', // Use JWTs for session tokens
        },
        // Define custom pages for sign-in, sign-out, error, etc.
        pages: {
          signIn: '/login', // Redirect users to /login page
          // error: '/auth/error', // Optional: Custom error page
          // verifyRequest: '/auth/verify-request', // Optional: For email provider
        },
        // Callbacks to control JWT and session content
        callbacks: {
          async jwt({ token, user }) {
            // Persist user ID and email into the JWT token after sign in
            if (user) {
              token.id = user.id;
              token.email = user.email; // Ensure email is in the token
            }
            return token;
          },
          async session({ session, token }) {
            // Add user ID and email from JWT token to the session object
            if (token && session.user) {
              session.user.id = token.id as string;
               session.user.email = token.email as string; // Ensure email is in the session
            }
            return session;
          },
        },
        // Enable debug messages in development
        debug: process.env.NODE_ENV === 'development',
        secret: process.env.NEXTAUTH_SECRET, // Ensure this is set in .env.local
      };
    • Create API Route Handler:

      typescript
      // src/app/api/auth/[...nextauth]/route.ts
      import NextAuth from 'next-auth';
      import { authOptions } from '@/lib/authOptions'; // Adjust path if needed
      
      const handler = NextAuth(authOptions);
      
      export { handler as GET, handler as POST };
    • Why these choices?

      • Prisma Adapter: Persists user data, essential for Credentials provider and managing user accounts long-term.
      • Credentials Provider: Allows traditional email/password login managed by your application. Requires password hashing (bcrypt).
      • JWT Strategy: Stateless session management using JSON Web Tokens stored in cookies. Suitable for serverless environments. Security Note: JWTs are not invalidatable server-side until expiry; for applications requiring immediate session revocation, consider database sessions (strategy: 'database').
      • Callbacks (jwt, session): Customize the token and session objects to include necessary user information (like user ID) accessible in your application.
      • Pages: Provides a better user experience by directing users to custom login pages instead of the default NextAuth pages.
  2. Set up Session Provider: Wrap your application layout with the NextAuth SessionProvider to make session data available globally via hooks like useSession.

    typescript
    // src/app/layout.tsx
    import type { Metadata } from 'next';
    import { Inter } from 'next/font/google';
    import './globals.css';
    import AuthProvider from '@/components/AuthProvider'; // We'll create this next
    
    const inter = Inter({ subsets: ['latin'] });
    
    export const metadata: Metadata = {
      title: 'Next.js Plivo MMS Sender',
      description: 'Send MMS securely with NextAuth and Plivo',
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <AuthProvider>{children}</AuthProvider> {/* Wrap with AuthProvider */}
          </body>
        </html>
      );
    }

    Create the AuthProvider component:

    typescript
    // src/components/AuthProvider.tsx
    'use client'; // This component uses client-side context
    
    import { SessionProvider } from 'next-auth/react';
    import React from 'react';
    
    interface Props {
      children: React.ReactNode;
    }
    
    export default function AuthProvider({ children }: Props) {
      return <SessionProvider>{children}</SessionProvider>;
    }
  3. Create Login Page: A simple login page using the Credentials provider.

    typescript
    // src/app/login/page.tsx
    'use client';
    
    import { useState, FormEvent } from 'react';
    import { signIn } from 'next-auth/react';
    import { useRouter } from 'next/navigation'; // Use 'next/navigation' in App Router
    
    export default function LoginPage() {
      const [email, setEmail] = useState('');
      const [password, setPassword] = useState('');
      const [error, setError] = useState<string | null>(null);
      const [isLoading, setIsLoading] = useState(false);
      const router = useRouter();
    
      const handleSubmit = async (e: FormEvent) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
    
        // Add simple client-side validation
        if (!email || !password) {
          setError('Email and password are required.');
          setIsLoading(false);
          return;
        }
    
        try {
          const result = await signIn('credentials', {
            redirect: false, // Prevent NextAuth from redirecting automatically
            email,
            password,
          });
    
          if (result?.error) {
            console.error('Sign in error:', result.error);
            setError('Invalid credentials. Please try again.'); // Provide generic error
            setIsLoading(false);
          } else if (result?.ok) {
            // Sign-in successful, redirect to the MMS sending page or dashboard
            router.push('/send-mms'); // Or '/dashboard' or wherever appropriate
          } else {
             setError('An unexpected error occurred during login.');
             setIsLoading(false);
          }
        } catch (err) {
           console.error('Login submission error:', err);
           setError('An unexpected error occurred. Please try again later.');
           setIsLoading(false);
        }
      };
    
      return (
        <div className="min-h-screen flex items-center justify-center bg-gray-100">
          <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
            <h1 className="text-2xl font-bold mb-6 text-center">Login</h1>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label
                  htmlFor="email"
                  className="block text-sm font-medium text-gray-700"
                >
                  Email
                </label>
                <input
                  type="email"
                  id="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  required
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black"
                />
              </div>
              <div>
                <label
                  htmlFor="password"
                  className="block text-sm font-medium text-gray-700"
                >
                  Password
                </label>
                <input
                  type="password"
                  id="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  required
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black"
                />
              </div>
              {error && (
                <p className="text-red-500 text-sm text-center">{error}</p>
              )}
              <button
                type="submit"
                disabled={isLoading}
                className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
              >
                {isLoading ? 'Logging in...' : 'Login'}
              </button>
            </form>
            <p className="mt-4 text-xs text-center text-gray-500">
                Login requires an existing user account. See note below on creating a test user.
            </p>
          </div>
        </div>
      );
    }

    Note on User Creation for Testing: Since we're using the CredentialsProvider, you need a user record in your database to log in. A production application would have a separate sign-up page and API route. For testing this guide, you can manually create a user:

    1. Ensure your database is running and accessible based on DATABASE_URL in .env.local.
    2. Run npx prisma studio in your terminal. This opens a web interface to your database.
    3. Navigate to the User model.
    4. Click "Add record".
    5. Enter an email address (e.g., test@example.com).
    6. For the password field, you need a bcrypt hash of the password you want to use. You cannot enter the plain password directly. Generate a hash using an online bcrypt generator or a simple Node.js script:
      javascript
      // Create a temporary file, e.g., temp_hash.js
      const bcrypt = require('bcrypt');
      const saltRounds = 10;
      const plainPassword = 'your_test_password'; // Replace with your desired password
      
      bcrypt.hash(plainPassword, saltRounds, function(err, hash) {
          if (err) {
              console.error("Hashing error:", err);
              return;
          }
          console.log('Copy this Hashed Password:', hash);
      });
      Run this script using node temp_hash.js. Copy the entire output hash string (it will look something like $2b$10$...).
    7. Paste the copied hash into the password field in Prisma Studio.
    8. Click "Save 1 record".
    9. You can now use the email and the plain password (e.g., your_test_password) on the login page.

    Production Sign-up Implementation: For production, create a sign-up API route (/api/auth/signup) that accepts email and password, validates input, hashes the password with bcrypt (minimum 10 salt rounds), creates the user in the database, and handles duplicate email errors appropriately.

3. Build the MMS Sending API Layer

Create the core endpoint that interacts with Plivo.

  1. Create the API Route:

    typescript
    // src/app/api/send-mms/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { getServerSession } from 'next-auth/next';
    import { authOptions } from '@/lib/authOptions'; // Adjust path
    import Plivo from 'plivo'; // Use default import
    
    // E.164 phone number validation regex
    const E164_REGEX = /^\+[1-9]\d{1,14}$/;
    
    export async function POST(req: NextRequest) {
      // 1. Authentication Check
      const session = await getServerSession(authOptions);
      if (!session || !session.user) {
        console.warn('Unauthorized attempt to send MMS');
        return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
      }
    
      // 2. Input Validation
      let body;
      try {
        body = await req.json();
      } catch (error) {
        console.error('Failed to parse request body:', error);
        return NextResponse.json({ success: false, error: 'Invalid request body' }, { status: 400 });
      }
    
      const { to, text, mediaUrl } = body;
    
      if (!to || typeof to !== 'string' || !text || typeof text !== 'string' || !mediaUrl || typeof mediaUrl !== 'string') {
         console.warn('Invalid input for send-mms:', { to, text, mediaUrl });
         return NextResponse.json({ success: false, error: 'Missing or invalid parameters: "to", "text", and "mediaUrl" are required strings.' }, { status: 400 });
      }
    
      // Validate phone number format (E.164)
      if (!E164_REGEX.test(to)) {
        console.warn('Invalid phone number format:', to);
        return NextResponse.json({
          success: false,
          error: 'Invalid phone number format. Must be in E.164 format (e.g., +14155551234).'
        }, { status: 400 });
      }
    
      // Validate text length (max 1,600 characters for MMS)
      if (text.length > 1600) {
        console.warn('Text exceeds maximum length:', text.length);
        return NextResponse.json({
          success: false,
          error: 'Message text exceeds maximum length of 1,600 characters.'
        }, { status: 400 });
      }
    
       // Basic validation for media URL format
      try {
        const url = new URL(mediaUrl);
        // Ensure URL is HTTP/HTTPS
        if (url.protocol !== 'http:' && url.protocol !== 'https:') {
          throw new Error('Invalid protocol');
        }
      } catch (error) {
          console.warn('Invalid media URL format:', mediaUrl);
          return NextResponse.json({ success: false, error: 'Invalid media URL format. Must be a valid HTTP/HTTPS URL.' }, { status: 400 });
      }
    
      // 3. Plivo Client Initialization & API Call
      const authId = process.env.PLIVO_AUTH_ID;
      const authToken = process.env.PLIVO_AUTH_TOKEN;
      const senderNumber = process.env.PLIVO_SENDER_NUMBER;
    
      if (!authId || !authToken || !senderNumber) {
        console.error('Plivo environment variables not configured');
        return NextResponse.json({ success: false, error: 'Server configuration error.' }, { status: 500 });
      }
    
      const client = new Plivo.Client(authId, authToken);
    
      try {
        console.log(`Attempting to send MMS from ${senderNumber} to ${to} by user ${session.user.email}`);
        const response = await client.messages.create({
          src: senderNumber, // Your Plivo MMS-enabled number
          dst: to,           // Recipient number
          text: text,        // Message body
          type: 'mms',       // Specify message type as MMS
          media_urls: [mediaUrl], // Array of public media URLs
          // media_ids: [] // Alternatively use media IDs if uploaded to Plivo
        });
    
        console.log('Plivo API Response:', response);
    
        // Check Plivo response structure for success indication.
        // Plivo typically includes message_uuid (as an array) on success.
        if (response && response.messageUuid && Array.isArray(response.messageUuid) && response.messageUuid.length > 0) {
            return NextResponse.json({ success: true, message: 'MMS sent successfully!', plivoResponse: response });
        } else {
            // Log unexpected Plivo response structure
            console.error('Unexpected Plivo response structure:', response);
            return NextResponse.json({ success: false, error: 'Failed to send MMS due to unexpected Plivo response.', plivoResponse: response }, { status: 500 });
        }
    
      } catch (error: any) {
        console.error(`Failed to send MMS via Plivo to ${to}:`, error);
        // Provide more specific error if possible, otherwise generic
        const errorMessage = error.message || 'An unknown error occurred while sending the MMS.';
        return NextResponse.json({ success: false, error: `Plivo API Error: ${errorMessage}`, details: error }, { status: 500 });
      }
    }
  2. API Endpoint Documentation:

    • Endpoint: POST /api/send-mms
    • Authentication: Requires active NextAuth session (JWT cookie).
    • Request Body (JSON):
      json
      {
        "to": "+15551234567",
        "text": "Check out this image!",
        "mediaUrl": "https://your-public-domain.com/image.jpg"
      }
    • Success Response (200 OK):
      json
      {
        "success": true,
        "message": "MMS sent successfully!",
        "plivoResponse": {
          "message": "message(s) queued",
          "messageUuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
          "apiId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        }
      }
    • Error Responses:
      • 400 Bad Request: Invalid input parameters or JSON body.
        json
        { "success": false, "error": "Missing or invalid parameters..." }
      • 401 Unauthorized: No active session or invalid session.
        json
        { "success": false, "error": "Unauthorized" }
      • 500 Internal Server Error: Plivo configuration error or Plivo API failure.
        json
        { "success": false, "error": "Server configuration error." }
        json
        { "success": false, "error": "Plivo API Error: [Specific error from Plivo]", "details": { ... } }
  3. Testing with curl: First, log in through the web interface to get a valid session cookie. Then, use your browser's developer tools (Network tab) to find the next-auth.session-token cookie value after logging in.

    bash
    # Replace YOUR_COOKIE_VALUE and other placeholders
    curl -X POST http://localhost:3000/api/send-mms \
      -H "Content-Type: application/json" \
      -H "Cookie: next-auth.session-token=YOUR_COOKIE_VALUE" \
      -d '{
            "to": "+1TARGETPHONE",
            "text": "Test MMS via curl!",
            "mediaUrl": "https://media.giphy.com/media/26gscSULUcfKU7dHq/source.gif"
          }'

    (Remember Plivo trial accounts can only send to verified numbers added in the Plivo console under "Sandbox Numbers")

4. Integrate with Plivo (Recap & Details)

You've already used the Plivo SDK in the API route. Here's a recap of the integration points with additional details:

  • Credentials: Store PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN securely in .env.local and access them via process.env. Obtain them directly from the Plivo Console Dashboard.
  • Sender Number: Store PLIVO_SENDER_NUMBER in .env.local. This must be an MMS-enabled number from your Plivo account, found under "Phone Numbers" → "Your Numbers". Use E.164 format (e.g., +14151234567).
  • SDK Usage: Install the plivo Node.js SDK (npm install plivo) and use it within the API route (/api/send-mms/route.ts) to instantiate the client (new Plivo.Client(...)) and send the message (client.messages.create(...)).
  • Media URLs: The media_urls parameter in client.messages.create expects an array of strings, where each string is a publicly accessible URL to a media file. Ensure the hosting server allows access from Plivo's servers. Alternatively, upload media to Plivo via their console ("Messaging" → "MMS Media Upload") or API and use the resulting media_id in the media_ids parameter.

MMS Media Specifications (Plivo MMS Documentation):

  • Supported Formats:
    • Images: JPEG, PNG, GIF (optimized for device compatibility automatically)
    • Other Media: Audio, video, and vCard files are supported but not optimized for device compatibility
  • Size Limits:
    • Maximum 10 media attachments per MMS message
    • Total size of message body text (max 1,600 characters = ~4.8KB) + all media files must be under 5MB
    • Messages exceeding 5MB fail with Plivo error code 120
    • For non-image files (audio/video), 600KB per file is recommended for best carrier compatibility
  • Media Hosting Requirements:
    • Media URLs must be publicly accessible via HTTP or HTTPS
    • The hosting server must respond to Plivo's requests (check firewall/access controls)
    • CORS configuration is not required for media hosting since Plivo fetches media server-side
    • Use reliable CDN hosting (S3, Cloudinary, Imgur) for production to ensure availability during message delivery

5. Implement Error Handling and Logging

Your API route includes basic error handling with the following improvements:

  • Authentication Errors: Check for a valid session using getServerSession and return a 401 if none exists.
  • Input Validation Errors: Check for the presence and basic type of to, text, and mediaUrl, returning a 400 if invalid. Includes E.164 phone number format validation, text length validation, and URL format validation.
  • Configuration Errors: Check if Plivo environment variables are set, returning a 500 if not.
  • Plivo API Errors: Wrap the client.messages.create call in a try...catch block. Log the error to the console (console.error) and return a 500 response containing the error message from Plivo.
  • Logging: Use console.log, console.warn, and console.error for basic logging on the server side. In production, integrate a dedicated logging library (like Pino or Winston) and forward logs to a centralized logging service (like Datadog, Logtail, Sentry).

Common Plivo Error Codes (Plivo Error Codes Reference):

  • Error Code 40: Invalid Source Number - The sender number is incorrectly formatted, not SMS/MMS-enabled, or not assigned to your account
  • Error Code 50: Invalid Destination Number - The recipient number is incorrectly formatted, not SMS-enabled, or is a landline
  • Error Code 120: MMS Message Payload Too Large - Total size exceeds 5MB limit
  • Error Code 130: Unsupported Message Media - One or more media attachments is of an unsupported type
  • Error Code 140: Message Media Processing Failed - Media URL is unreachable or file data is incorrectly formatted
  • Error Code 420: Message Expired - Message remained in queue longer than 3 hours (default expiry)
  • Error Code 900: Insufficient Credit - Account lacks required credits
  • Error Code 1000: Unknown Error - Failure reason unknown; contact Plivo support if persistent

Structured Error Response Pattern:

typescript
// Example error response structure
interface ErrorResponse {
  success: false;
  error: string;           // Human-readable error message
  code?: string;           // Application-specific error code
  plivoErrorCode?: number; // Plivo error code if applicable
  details?: any;           // Additional error details for debugging
}

Test Error Scenarios:

  • Unauthorized: Make a curl request without the Cookie header.
  • Bad Request: Send a curl request with missing fields (e.g., no mediaUrl) or malformed JSON. Send an invalid URL format. Send an invalid phone number format (e.g., missing country code).
  • Plivo Error: Temporarily change PLIVO_AUTH_TOKEN in .env.local to an invalid value and restart the server, then try sending. Send to an invalid/non-existent phone number format. Use an invalid/inaccessible mediaUrl.

6. Review Database Schema and Data Layer

You set up Prisma earlier for user authentication data persistence via the PrismaAdapter.

  • Schema: The prisma/schema.prisma file defines the User, Account, Session, and VerificationToken models required by next-auth/prisma-adapter. You added an optional password field to the User model for the Credentials provider.
  • Data Access: Prisma Client (@prisma/client) is used implicitly by the PrismaAdapter to read/write user and session data. The authorize function within the Credentials provider configuration explicitly uses prisma.user.findUnique to look up users by email.
  • Migrations: Use npx prisma migrate dev to manage database schema changes based on the schema.prisma file.

7. Build the Frontend MMS Sending Page

Create a protected page where authenticated users can send MMS messages.

  1. Create the Send MMS Page:

    typescript
    // src/app/send-mms/page.tsx
    'use client';
    
    import { useState, FormEvent } from 'react';
    import { useSession } from 'next-auth/react';
    import { useRouter } from 'next/navigation';
    
    export default function SendMMSPage() {
      const { data: session, status } = useSession();
      const router = useRouter();
      const [to, setTo] = useState('');
      const [text, setText] = useState('');
      const [mediaUrl, setMediaUrl] = useState('');
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [success, setSuccess] = useState<string | null>(null);
    
      // Redirect to login if not authenticated
      if (status === 'loading') {
        return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
      }
    
      if (status === 'unauthenticated') {
        router.push('/login');
        return null;
      }
    
      const handleSubmit = async (e: FormEvent) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
        setSuccess(null);
    
        try {
          const response = await fetch('/api/send-mms', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ to, text, mediaUrl }),
          });
    
          const data = await response.json();
    
          if (response.ok && data.success) {
            setSuccess('MMS sent successfully!');
            // Reset form
            setTo('');
            setText('');
            setMediaUrl('');
          } else {
            setError(data.error || 'Failed to send MMS');
          }
        } catch (err) {
          console.error('Error sending MMS:', err);
          setError('An unexpected error occurred. Please try again.');
        } finally {
          setIsLoading(false);
        }
      };
    
      return (
        <div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
          <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-8">
            <div className="mb-6">
              <h1 className="text-2xl font-bold text-center">Send MMS</h1>
              <p className="text-sm text-gray-600 text-center mt-2">
                Logged in as: {session?.user?.email}
              </p>
            </div>
    
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label htmlFor="to" className="block text-sm font-medium text-gray-700">
                  Recipient Phone Number
                </label>
                <input
                  type="tel"
                  id="to"
                  value={to}
                  onChange={(e) => setTo(e.target.value)}
                  placeholder="+15551234567"
                  required
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border"
                />
                <p className="mt-1 text-xs text-gray-500">
                  Must be in E.164 format (e.g., +14155551234)
                </p>
              </div>
    
              <div>
                <label htmlFor="text" className="block text-sm font-medium text-gray-700">
                  Message Text
                </label>
                <textarea
                  id="text"
                  value={text}
                  onChange={(e) => setText(e.target.value)}
                  placeholder="Your message here..."
                  required
                  rows={4}
                  maxLength={1600}
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border"
                />
                <p className="mt-1 text-xs text-gray-500">
                  {text.length}/1600 characters
                </p>
              </div>
    
              <div>
                <label htmlFor="mediaUrl" className="block text-sm font-medium text-gray-700">
                  Media URL
                </label>
                <input
                  type="url"
                  id="mediaUrl"
                  value={mediaUrl}
                  onChange={(e) => setMediaUrl(e.target.value)}
                  placeholder="https://example.com/image.jpg"
                  required
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border"
                />
                <p className="mt-1 text-xs text-gray-500">
                  Publicly accessible URL (JPEG, PNG, GIF supported)
                </p>
              </div>
    
              {error && (
                <div className="rounded-md bg-red-50 p-4">
                  <p className="text-sm text-red-800">{error}</p>
                </div>
              )}
    
              {success && (
                <div className="rounded-md bg-green-50 p-4">
                  <p className="text-sm text-green-800">{success}</p>
                </div>
              )}
    
              <button
                type="submit"
                disabled={isLoading}
                className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {isLoading ? 'Sending...' : 'Send MMS'}
              </button>
            </form>
    
            <div className="mt-6 text-center">
              <button
                onClick={() => router.push('/')}
                className="text-sm text-indigo-600 hover:text-indigo-800"
              >
                Back to Home
              </button>
            </div>
          </div>
        </div>
      );
    }
  2. Create a Simple Home Page:

    typescript
    // src/app/page.tsx
    'use client';
    
    import { useSession, signOut } from 'next-auth/react';
    import { useRouter } from 'next/navigation';
    
    export default function Home() {
      const { data: session, status } = useSession();
      const router = useRouter();
    
      return (
        <div className="min-h-screen bg-gray-100 flex items-center justify-center">
          <div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full text-center">
            <h1 className="text-3xl font-bold mb-6">Plivo MMS Sender</h1>
    
            {status === 'loading' && <p>Loading...</p>}
    
            {status === 'authenticated' && session && (
              <div className="space-y-4">
                <p className="text-gray-700">
                  Welcome, <span className="font-semibold">{session.user?.email}</span>!
                </p>
                <button
                  onClick={() => router.push('/send-mms')}
                  className="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition"
                >
                  Send MMS
                </button>
                <button
                  onClick={() => signOut()}
                  className="w-full py-2 px-4 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition"
                >
                  Sign Out
                </button>
              </div>
            )}
    
            {status === 'unauthenticated' && (
              <div className="space-y-4">
                <p className="text-gray-700 mb-4">
                  Please log in to send MMS messages.
                </p>
                <button
                  onClick={() => router.push('/login')}
                  className="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition"
                >
                  Log In
                </button>
              </div>
            )}
          </div>
        </div>
      );
    }

8. Testing and Troubleshooting

Common Issues and Solutions:

  1. "Unauthorized" Error When Sending MMS:

    • Cause: Session cookie not being sent or invalid session
    • Solution: Ensure you're logged in. Check browser developer tools → Application → Cookies for next-auth.session-token. Clear cookies and log in again if needed.
  2. "Invalid phone number format" Error:

    • Cause: Phone number not in E.164 format
    • Solution: Ensure the number includes + and country code (e.g., +14155551234 for US numbers)
  3. Plivo Error Code 40 (Invalid Source Number):

    • Cause: PLIVO_SENDER_NUMBER is incorrect or doesn't have MMS capability
    • Solution: Verify the number in Plivo Console → Phone Numbers → Your Numbers. Ensure it has MMS capability enabled.
  4. Plivo Error Code 120 (Payload Too Large):

    • Cause: Total MMS size exceeds 5MB
    • Solution: Reduce image file size or use fewer/smaller attachments
  5. Plivo Error Code 140 (Media Processing Failed):

    • Cause: Media URL is not publicly accessible or returns an error
    • Solution: Test the media URL in a browser. Ensure it returns the image with proper MIME type headers. Check hosting service access permissions.
  6. Messages Only Send to One Number (Trial Account):

    • Cause: Trial accounts can only send to verified sandbox numbers
    • Solution: Add recipient numbers in Plivo Console → Sandbox Numbers, or upgrade to a paid account
  7. TypeScript Errors with Session User ID:

    • Cause: Missing type augmentation for NextAuth
    • Solution: Ensure src/types/next-auth.d.ts exists with proper module augmentation (see Section 2)

Testing Checklist:

  • User can log in with valid credentials
  • User cannot access /send-mms without authentication
  • MMS sends successfully with valid inputs
  • Proper error shown for invalid phone number format
  • Proper error shown for invalid media URL
  • Proper error shown when message text exceeds 1,600 characters
  • Loading state shows during message sending
  • Success message displays after successful send
  • User can send multiple messages in succession

9. Deployment Best Practices

Environment Variables:

  • Set all environment variables (NEXTAUTH_SECRET, NEXTAUTH_URL, PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SENDER_NUMBER, DATABASE_URL) in your deployment platform (Vercel, Railway, AWS, etc.)
  • Update NEXTAUTH_URL to your production domain (e.g., https://yourdomain.com)
  • Use your platform's secrets management for sensitive values

Database Considerations:

  • For serverless deployments (Vercel, Netlify), use a serverless-compatible database (Neon, PlanetScale, Supabase)
  • Enable connection pooling to prevent exhausting database connections
  • Run npx prisma generate during build process
  • Run npx prisma migrate deploy for production migrations (not migrate dev)

Security Hardening:

  • Enable HTTPS only (enforce in Next.js middleware or at CDN level)
  • Implement rate limiting on /api/send-mms to prevent abuse (use libraries like @upstash/ratelimit or middleware)
  • Add CSRF protection if not using default NextAuth cookies
  • Monitor failed login attempts and implement account lockout
  • Regularly rotate NEXTAUTH_SECRET and Plivo credentials
  • Set appropriate CORS headers if building a separate frontend

Monitoring:

  • Integrate application monitoring (Sentry, LogRocket) for error tracking
  • Set up logging aggregation (Datadog, Logtail) for production logs
  • Monitor Plivo usage and costs in the Plivo Console
  • Set up alerts for API errors, authentication failures, and failed message sends

This completes your MMS sending application with Plivo, Next.js, and NextAuth. You now have a secure, production-ready foundation for sending multimedia messages from your web application.

Frequently Asked Questions

How to send MMS with Plivo and Next.js?

You can send MMS messages by creating a secure API endpoint in your Next.js application using the Plivo Node.js SDK. This endpoint, protected by NextAuth.js, handles the interaction with the Plivo API to send multimedia messages after verifying user authentication and input validation. A simple frontend interface allows users to input recipient details, text, and media URL.

What is NextAuth used for in this MMS setup?

NextAuth.js is used for user authentication. It ensures only logged-in users can access and trigger the MMS sending functionality via the secure API endpoint. This is important for protecting your Plivo credentials and preventing unauthorized message sending.

Why use Plivo for sending MMS from a web app?

Plivo is a cloud communications platform providing reliable messaging services. Its developer-friendly Node.js SDK simplifies the integration with your Next.js application, handling the complexities of sending MMS messages. Plivo offers various features and global reach for messaging.

When should I initialize the Plivo client in Next.js?

The Plivo client should be initialized within the server-side API route handler that processes the MMS sending request. This ensures the client is created for each request and that API credentials are handled securely on the server.

What Plivo credentials are needed for sending MMS?

You'll need your Plivo AUTH ID, AUTH TOKEN, and an MMS-enabled Plivo phone number. These are found in your Plivo Console dashboard. Store these securely as environment variables (`PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`, `PLIVO_SENDER_NUMBER`) in a `.env.local` file, which should *never* be committed to version control.

How to handle Plivo API errors in Next.js?

Wrap the `client.messages.create` call in a `try...catch` block within your API route handler. Log the error using `console.error` and return a 500 response with a user-friendly error message. Include details about the error from Plivo for debugging when possible.

How to protect the send MMS API route in Next.js?

Use NextAuth's `getServerSession` function within the API route handler (`/api/send-mms`). This function checks for a valid NextAuth session based on the JWT cookie. Return a 401 Unauthorized response if no session exists.

What is the role of Prisma in sending MMS with Plivo?

Prisma is used for user data management, especially when using authentication providers like Credentials. It stores user data, sessions, accounts, and other authentication-related information. Prisma isn't directly involved in the MMS sending process via Plivo but manages users for NextAuth.

How to structure the request body for the send MMS API?

The request body should be JSON with three fields: `to` (recipient's phone number in E.164 format), `text` (the message body), and `mediaUrl` (a publicly accessible URL of the media file).

What are the prerequisites for setting up MMS sending?

You need Node.js v18+, npm or yarn, a Plivo account with an MMS-enabled number, basic understanding of React/Next.js, and a publicly accessible media URL. You will also need the Plivo Node.js SDK.

How to get a Plivo MMS enabled number?

Log in to your Plivo console and navigate to "Phone Numbers" -> "Your Numbers" to check your existing numbers. If you don't have an MMS-enabled number, buy one by going to "Buy Numbers" and filtering the search options by MMS capability.

What media formats are supported for MMS with Plivo?

Plivo supports common image formats like JPEG, PNG, and GIF for MMS messages. Ensure the URL you provide in the `mediaUrl` parameter points to a publicly accessible file of a supported format. Plivo allows media upload through their console or API.

How to set up NextAuth for protecting the API endpoint?

Create a NextAuth API route handler (`/api/auth/[...nextauth]/route.ts`) with a configuration file specifying providers, session strategy, and database adapter if needed. Use `getServerSession` in the MMS API route to check for an active session.

Can I test sending MMS locally during development?

Yes, you can test sending MMS during development using tools like `curl` to send requests to the `POST /api/send-mms` endpoint. Ensure Plivo is configured to send to sandbox or test numbers.