code examples

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

How to Send MMS with Plivo in Next.js: Complete 2025 Guide

Learn how to send MMS messages with Plivo API in Next.js. Step-by-step tutorial with TypeScript, App Router, authentication, and production deployment for multimedia messaging.

Send MMS with Plivo, Node.js, and Next.js

Learn how to send MMS messages with Plivo in a Next.js application. This comprehensive tutorial shows you how to build a production-ready MMS sender using the Plivo API, Node.js, and the Next.js App Router. You'll implement multimedia messaging with TypeScript, proper error handling, and secure credential management for US and Canadian markets.

This guide walks you through creating a web interface where users can send MMS (Multimedia Messaging Service) containing text and media attachments. The application uses a Next.js API route that connects to the Plivo Node.js SDK v4.x, supporting JPEGs, GIFs, MP3 audio, and MP4 video files—perfect for marketing campaigns, notifications, and customer engagement.

Prerequisites

Before you start building your MMS application, ensure you have:

  1. Node.js and npm/yarn: Installed on your development machine (Node.js v18 or later recommended). Download Node.js
  2. Plivo Account: A registered Plivo account. Sign up for Plivo.
  3. Plivo Auth ID and Auth Token: Found on your Plivo Console dashboard homepage.
  4. MMS-Enabled Plivo Phone Number: You need a Plivo phone number capable of sending MMS messages. MMS is currently supported only for US and Canadian numbers (long code, toll-free, and short code). Plivo supports sending MMS to major carriers including AT&T, Verizon, T-Mobile, Rogers, Bell, Fido, Telus, and Wind Canada. You can rent one via the Plivo Console under "Phone Numbers" > "Buy Numbers". Ensure MMS capability is listed for the number.
  5. Verified Destination Number (Trial Accounts): If using a Plivo trial account, the destination phone number must be verified (sandboxed) in the Plivo Console under "Phone Numbers" > "Sandbox Numbers".
  6. Code Editor: Such as VS Code.
  7. Git: For version control (optional but recommended).

Plivo SDK Version: This guide uses Plivo Node.js SDK v4.x (latest: 4.74.0 as of January 2025). Install via npm install plivo.

Supported Media Formats: Plivo supports JPEGs, GIFs, MP3 audio, and MP4 video files for MMS.

What Will You Build in This Tutorial?

Goal: Create a functional Next.js application that sends MMS messages (text + image/GIF/audio/video) via the Plivo API.

Problem Solved: Provides a clear, secure, and scalable way to integrate programmatic MMS sending into a modern web application framework like Next.js.

Technologies Used:

  • Next.js: A React framework for building server-side rendered (SSR) and static web applications. Chosen for its robust features, API routes, and developer experience.
  • React: For building the user interface.
  • Node.js: The runtime environment for Next.js and the Plivo SDK.
  • Plivo Node.js SDK: Simplifies interaction with the Plivo REST API.
  • Plivo API: The underlying communications API for sending MMS.

System Architecture:

The data flow is as follows:

  1. The user interacts with a form in their browser (React component in pages/index.js).
  2. On submission, the frontend sends a POST request containing the recipient number (to), message text (text), and media URL (mediaUrl) to a Next.js API route (/api/send-mms).
  3. The API route (running server-side on Node.js) receives the request, validates the data, and uses the Plivo Node.js SDK (with Auth ID/Token) to make an authenticated request to the Plivo API.
  4. The Plivo API processes the request and sends the MMS message to the recipient.
  5. Plivo returns a response (e.g., message UUID, status) to the API route.
  6. The API route sends a success or error response back to the frontend.

Expected Outcome: A running Next.js application with a webpage containing a form. Submitting the form successfully sends an MMS message containing the specified text and media to the target phone number.

Setting Up Your Next.js Project

Let's start by creating a new Next.js project and installing the necessary Plivo SDK.

  1. Create a Next.js App: Open your terminal and run the following command. Replace plivo-mms-sender with your desired project name. We'll use TypeScript for enhanced type safety, but you can opt for JavaScript if preferred.

    bash
    npx create-next-app@latest plivo-mms-sender --typescript --eslint --tailwind --src-dir --app --import-alias "@/*"
    # Follow the prompts (you can accept defaults)
    • --typescript: Initializes the project with TypeScript.
    • --eslint: Includes ESLint for code linting.
    • --tailwind: Sets up Tailwind CSS for styling (optional, but useful for UI).
    • --src-dir: Creates a src directory for organizing code.
    • --app: Uses the newer App Router (recommended).
    • --import-alias "@/*": Configures path aliases.
  2. Navigate to Project Directory:

    bash
    cd plivo-mms-sender
  3. Install Plivo Node.js SDK: Add the Plivo helper library to your project dependencies.

    bash
    npm install plivo
    # or using yarn:
    # yarn add plivo
  4. Set Up Environment Variables: Sensitive credentials like API keys should never be hardcoded. We'll use environment variables. Create a file named .env.local in the root of your project. Important: Add .env.local to your .gitignore file to prevent committing secrets.

    • Create the .gitignore file if it doesn't exist or add the following line:

      text
      # .gitignore
      .env*.local
    • Create the .env.local file:

      bash
      touch .env.local
    • Add your Plivo credentials and sender ID (your Plivo number) to .env.local:

      dotenv
      # .env.local
      # Plivo Credentials - Found on Plivo Console Dashboard
      PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
      PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
      
      # Your MMS-Enabled Plivo Phone Number (Digits only, e.g., 14155551212)
      PLIVO_SENDER_ID=YOUR_PLIVO_PHONE_NUMBER_DIGITS_ONLY
    • Explanation:

      • PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN: Used by the Plivo SDK to authenticate API requests. Obtained from the Plivo Console.
      • PLIVO_SENDER_ID: The src (source) phone number for sending MMS. Must be an MMS-enabled number rented from Plivo, provided here as digits only (e.g., 14155551212).
      • Next.js automatically loads variables prefixed with NEXT_PUBLIC_ into the browser environment. Since our API key usage is server-side only (in the API route), we do not need the NEXT_PUBLIC_ prefix, keeping our credentials secure.
      • Important: After creating or modifying the .env.local file, you must restart your Next.js development server (npm run dev) for the changes to take effect.
  5. Project Structure Overview (Simplified): Your relevant project structure should look something like this:

    plaintext
    plivo-mms-sender/
    └── src/
        └── app/
            ├── api/
            │   └── send-mms/
            │       └── route.ts       # <-- Our API endpoint logic
            ├── layout.tsx
            └── page.tsx             # <-- Our Frontend UI
    └── .env.local                   # <-- Plivo credentials (Git ignored)
    └── .gitignore
    └── next.config.mjs
    └── package.json
    └── tsconfig.json
    • src/app/api/send-mms/route.ts: This file will contain the server-side logic to handle requests and interact with the Plivo API.
    • src/app/page.tsx: This will contain the React component for our user interface (the form).
    • .env.local: Stores our sensitive credentials.

Implementing the MMS API Endpoint

Now, let's build the API route that will handle the MMS sending logic using the Next.js App Router.

  1. Create the API Route File: Create the directory structure and file src/app/api/send-mms/route.ts.

  2. Implement the API Logic: Paste the following code into src/app/api/send-mms/route.ts:

    typescript
    // src/app/api/send-mms/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import * as plivo from 'plivo';
    
    // Validate environment variables on server start
    const authId = process.env.PLIVO_AUTH_ID;
    const authToken = process.env.PLIVO_AUTH_TOKEN;
    const senderId = process.env.PLIVO_SENDER_ID;
    
    if (!authId || !authToken || !senderId) {
        console.error("Plivo credentials or Sender ID are missing in environment variables.");
        // In a real app, you might throw an error or handle this more gracefully
        // For this example, we log and proceed, but requests will fail later.
    }
    
    // Basic regex for E.164-like format (allows optional '+') for destination
    const phoneRegexDest = /^\+?[1-9]\d{1,14}$/;
    // Stricter regex for Sender ID from .env (digits only expected)
    const phoneRegexSrc = /^[1-9]\d{9,14}$/; // Adjust min/max length as needed
    
    export async function POST(request: NextRequest) {
        // Ensure Plivo credentials are loaded correctly within the request context
        if (!authId || !authToken || !senderId) {
            return NextResponse.json(
                { error: 'Server configuration error: Plivo credentials missing.' },
                { status: 500 }
            );
        }
    
        // Validate Sender ID format from .env.local early
        if (!phoneRegexSrc.test(senderId)) {
             console.error(`Invalid PLIVO_SENDER_ID format in .env.local: ${senderId}. Expected digits only (e.g., 14155551212).`);
             return NextResponse.json(
                { error: 'Server configuration error: Invalid Sender ID format.' },
                { status: 500 }
            );
        }
    
        let client: plivo.Client | null = null;
        try {
            client = new plivo.Client(authId, authToken);
        } catch (error) {
            console.error("Failed to initialize Plivo client:", error);
            return NextResponse.json(
                { error: 'Failed to initialize Plivo client.' },
                { status: 500 }
            );
        }
    
        try {
            const body = await request.json();
            const { to, text, mediaUrl } = body;
    
            // --- Input Validation ---
            if (!to || !text || !mediaUrl) {
                return NextResponse.json(
                    { error: 'Missing required fields: to, text, mediaUrl' },
                    { status: 400 } // Bad Request
                );
            }
    
            // Basic E.164-like check. Consider using libphonenumber-js for robust validation.
            if (!phoneRegexDest.test(to)) {
                 return NextResponse.json(
                    { error: 'Invalid "to" phone number format. Use E.164 format (e.g., +14155551234).' },
                    { status: 400 }
                );
            }
    
            // Basic URL format check
             try {
                new URL(mediaUrl);
            } catch (_) {
                return NextResponse.json(
                    { error: 'Invalid "mediaUrl" format.' },
                    { status: 400 }
                );
            }
            // --- End Input Validation ---
    
            console.log(`Attempting to send MMS to: ${to} from: ${senderId}`);
    
            const response = await client.messages.create(
                senderId, // src: Your Plivo number (digits only from .env.local)
                to,       // dst: Recipient number (E.164 format from request body)
                text,     // text: Message content from request body
                { // options
                    type: "mms",
                    media_urls: [mediaUrl] // Media URL from request body
                }
                // Plivo SDK v4+ typically handles formatting 'src' if needed, but providing digits is safest.
            );
    
            console.log("Plivo API Response:", response);
    
            // Successfully sent
            return NextResponse.json(
                {
                    success: true,
                    message: `MMS queued successfully to ${to}`,
                    // Plivo Node SDK v4+ returns messageUuid in an array, even for single sends.
                    message_uuid: response.messageUuid?.[0]
                },
                { status: 200 } // OK
            );
    
        } catch (error: any) {
            console.error("Error sending MMS via Plivo:", error);
    
            // Provide more specific feedback if possible
            let errorMessage = 'Failed to send MMS.';
            if (error.message) {
                 errorMessage = `Failed to send MMS: ${error.message}`;
            }
            // Plivo errors often have more details in error.response or similar properties.
            // Consider logging the full error object for debugging.
    
            return NextResponse.json(
                { error: errorMessage },
                { status: 500 } // Internal Server Error
            );
        }
    }
    
    // Optional: Handle other methods like GET if needed, otherwise they default to 405 Method Not Allowed
    export async function GET() {
        return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 });
    }
    • Explanation:
      • We import NextRequest, NextResponse from next/server and the plivo SDK.
      • We read the Plivo credentials and sender ID from process.env. Crucially, this happens server-side, protecting your keys. We add checks to ensure these variables are present.
      • The POST function is the handler for POST requests to /api/send-mms.
      • It initializes the Plivo Client using the Auth ID and Token.
      • It parses the incoming JSON request body to get to, text, and mediaUrl.
      • Input Validation: It performs basic checks:
        • Ensures all required fields are present.
        • Validates the format of the to phone number (E.164-like) and mediaUrl.
        • Validates the format of the senderId from .env.local (digits only).
        • Returns a 400 Bad Request or 500 Internal Server Error if validation fails.
      • It calls client.messages.create() with:
        • src: Your Plivo number (senderId, digits only).
        • dst: The recipient number (to).
        • text: The message text (text).
        • options: An object specifying the message type as "mms" and providing the media_urls as an array containing the mediaUrl.
      • Error Handling: It uses a try...catch block to handle potential errors during the API call (e.g., invalid credentials, network issues, Plivo API errors). It logs the error server-side and returns a 500 Internal Server Error response with an error message.
      • Success Response: If the Plivo API call is successful (doesn't throw an error), it returns a 200 OK response with a success message and the message_uuid provided by Plivo (accessing the first element of the messageUuid array).
      • A basic GET handler is added to return 405 Method Not Allowed if someone tries to access the endpoint via GET.

Building the Frontend Interface

Now, let's create the form in our main page component (src/app/page.tsx) to interact with the API endpoint.

  1. Clear Boilerplate and Add Form: Replace the contents of src/app/page.tsx with the following code:

    typescript
    // src/app/page.tsx
    'use client'; // Required for components with hooks (useState, useEffect)
    
    import { useState, FormEvent } from 'react';
    
    export default function HomePage() {
        const [to, setTo] = useState('');
        const [text, setText] = useState('');
        // Default to a known working GIF for ease of testing
        const [mediaUrl, setMediaUrl] = useState('https://media.giphy.com/media/tPerToLqZRRFa8/giphy.gif');
        const [statusMessage, setStatusMessage] = useState('');
        const [isLoading, setIsLoading] = useState(false);
        const [isError, setIsError] = useState(false);
    
        const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
            event.preventDefault(); // Prevent default form submission
            setIsLoading(true);
            setStatusMessage('');
            setIsError(false);
    
            try {
                const response = await fetch('/api/send-mms', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ to, text, mediaUrl }),
                });
    
                const result = await response.json();
    
                if (!response.ok) {
                    // Handle HTTP errors (4xx, 5xx)
                    throw new Error(result.error || `HTTP error! status: ${response.status}`);
                }
    
                // Success
                setStatusMessage(`Success: ${result.message} (UUID: ${result.message_uuid})`);
                // Optionally clear the form on success
                // setTo('');
                // setText('');
                // setMediaUrl('https://media.giphy.com/media/tPerToLqZRRFa8/giphy.gif'); // Reset to default or empty
    
            } catch (error: any) {
                console.error('Submission error:', error);
                setStatusMessage(`Error: ${error.message || 'Failed to send MMS.'}`);
                setIsError(true);
            } finally {
                setIsLoading(false);
            }
        };
    
        return (
            <main className="flex min-h-screen flex-col items-center justify-center p-8 bg-gray-100">
                <div className="w-full max-w-md p-8 space-y-6 bg-white rounded-lg shadow-md">
                    <h1 className="text-2xl font-bold text-center text-gray-800">Send Plivo MMS</h1>
    
                    <form onSubmit={handleSubmit} className="space-y-4">
                        <div>
                            <label htmlFor="to" className="block text-sm font-medium text-gray-700">
                                To Phone Number:
                            </label>
                            <input
                                type="tel"
                                id="to"
                                value={to}
                                onChange={(e) => setTo(e.target.value)}
                                required
                                placeholder="+14155551234"
                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                            />
                             <p className="mt-1 text-xs text-gray-500">Include country code (e.g., +1 for US/Canada).</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)}
                                required
                                rows={3}
                                placeholder="Your message here..."
                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                            />
                        </div>
    
                         <div>
                            <label htmlFor="mediaUrl" className="block text-sm font-medium text-gray-700">
                                Media URL (Image/GIF):
                            </label>
                            <input
                                type="url"
                                id="mediaUrl"
                                value={mediaUrl}
                                onChange={(e) => setMediaUrl(e.target.value)}
                                required
                                placeholder="https://example.com/image.gif"
                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                            />
                             <p className="mt-1 text-xs text-gray-500">Must be a publicly accessible URL.</p>
                        </div>
    
                        <button
                            type="submit"
                            disabled={isLoading}
                            className="w-full flex justify-center 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>
    
                    {statusMessage && (
                        <div className={`mt-4 p-3 rounded-md text-sm ${isError ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
                            {statusMessage}
                        </div>
                    )}
                </div>
            </main>
        );
    }
    • Explanation:
      • 'use client': This directive marks the component as a Client Component, necessary because we use React hooks (useState).
      • State Variables: useState hooks manage the form inputs (to, text, mediaUrl), loading state (isLoading), error state (isError), and status messages (statusMessage).
      • handleSubmit Function:
        • Prevents the default form submission behavior.
        • Sets loading state and clears previous status messages.
        • Uses the fetch API to make a POST request to our /api/send-mms endpoint.
        • Sends the form data as a JSON string in the request body.
        • Sets the Content-Type header to application/json.
        • Processes the response:
          • If the response is not ok (status code outside 200-299), it throws an error using the message from the API response.
          • If successful, it updates the status message with success details from the API.
        • Handles errors using a catch block, updating the status message accordingly.
        • Uses a finally block to ensure the loading state is reset regardless of success or failure.
      • JSX Form:
        • A standard HTML form element with an onSubmit handler linked to handleSubmit.
        • Input fields for "To Phone Number", "Message Text", and "Media URL", bound to their respective state variables using value and onChange. Basic HTML5 validation (required, type="tel", type="url") is included.
        • A submit button that is disabled during loading.
        • A status message area that conditionally renders based on statusMessage, styled differently for success and error messages using Tailwind CSS classes.

Integrating with Plivo API

This section focuses specifically on the Plivo integration points and best practices for connecting your Next.js application to Plivo's messaging service.

  1. Obtaining Credentials (Recap):

    • Navigate to the Plivo Console.
    • Your Auth ID and Auth Token are displayed prominently on the main dashboard page.
    • Important: Treat your Auth Token like a password – keep it secure and do not share it or commit it to version control.
  2. Renting/Verifying Phone Number:

    • Go to "Phone Numbers" in the Plivo Console.
    • Rent Number: Click "Buy Numbers". Search by country (US or Canada for MMS), select features (ensure MMS is checked), choose a number, and complete the purchase.
    • Verify Sender ID: Copy the full phone number (e.g., 14155551212) that you rented and paste it as digits only as the value for PLIVO_SENDER_ID in your .env.local file.
    • Verify Destination Number (Trial Accounts Only): Go to "Phone Numbers" > "Sandbox Numbers". Click "Add Sandbox Number", enter the destination phone number you want to test sending to, and follow the verification process (usually involves receiving a call or SMS with a code). Only verified numbers can receive messages from trial accounts.
  3. Secure Storage (.env.local):

    • As done in Step 1, store PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, and PLIVO_SENDER_ID (digits only) in the .env.local file.
    • Ensure .env.local is listed in your .gitignore file. Next.js automatically makes these variables available server-side via process.env. Remember to restart the dev server after changes.
  4. SDK Initialization:

    • In src/app/api/send-mms/route.ts, the Plivo client is initialized securely using the environment variables:

      typescript
      const authId = process.env.PLIVO_AUTH_ID;
      const authToken = process.env.PLIVO_AUTH_TOKEN;
      // ... validation ...
      client = new plivo.Client(authId, authToken);
    • This happens server-side only, protecting your credentials.

  5. API Call:

    • The core interaction happens via client.messages.create(...) within the API route. The parameters (src, dst, text, type, media_urls) map directly to the Plivo Message API requirements for sending an MMS. The src parameter uses the digits-only senderId from .env.local.

Error Handling and Logging Best Practices

Our current implementation includes basic error handling and logging. Let's refine it for production use.

  1. Consistent Error Strategy (API Route):

    • Validation Errors: Return 400 Bad Request with a clear error message indicating the specific field or format issue (as implemented).
    • Configuration Errors: If server-side config (like missing env vars or invalid sender ID format) is the issue, return 500 Internal Server Error and log the specific detail server-side (as implemented). Avoid exposing sensitive config details to the client.
    • Plivo API Errors: Catch errors from client.messages.create. Log the full error server-side for debugging. Return a generic 500 Internal Server Error to the client, potentially including a sanitized error message from Plivo if available and safe (e.g., error.message).
    • Initialization Errors: Handle errors during new plivo.Client() – return 500.
  2. Logging (API Route):

    • We use console.log() for basic informational messages (attempting send, Plivo response) and console.error() for critical issues (missing credentials, Plivo API errors, validation failures).
    • Enhancement: For production, use a structured logging library (e.g., pino, winston) to output logs in JSON format. This makes them easier to parse, filter, and analyze with log management tools (like Datadog, Logtail, Papertrail). Include request IDs for tracing.
    typescript
    // Example using basic console logging structure
    console.log(JSON.stringify({ level: 'info', message: 'Attempting to send MMS', data: { to, from: senderId } }));
    console.error(JSON.stringify({ level: 'error', message: 'Plivo API Error', error: error.message, stack: error.stack, plivoDetails: error.response?.data })); // Log more details if available
  3. Retry Mechanisms (Conceptual):

    • Sending an MMS is generally idempotent if using unique identifiers, but Plivo handles retries internally for deliverability to some extent. Implementing client-side retries in our API route for transient network errors or temporary Plivo issues (like 503 Service Unavailable) can improve robustness.

    • Strategy: Use a library like async-retry or implement manually.

      • Retry only on specific error types (e.g., network errors, 5xx errors from Plivo). Do not retry on 4xx errors (bad request, invalid number).
      • Implement exponential backoff (e.g., wait 1s, then 2s, then 4s) with jitter (random variation) to avoid overwhelming the API.
      • Set a maximum number of retries (e.g., 3).
    • Setup: Install the library:

      bash
      npm install async-retry @types/async-retry
      # or
      # yarn add async-retry @types/async-retry
    • Implementation:

      typescript
      // Conceptual example using async-retry
      import retry from 'async-retry';
      // Potentially move Plivo client initialization inside the retry scope if needed
      
      // Inside the POST function, wrap the Plivo call
      try {
         const response = await retry(
             async (bail, attempt) => { // Changed variable name for clarity
                 console.log(`Attempt #${attempt}...`);
                 try {
                    // Ensure client is initialized if needed within retry scope
                    if (!client) throw new Error("Plivo client not initialized");
      
                    const plivoResponse = await client.messages.create(
                        senderId, to, text, { type: "mms", media_urls: [mediaUrl] }
                    );
                    return plivoResponse; // Success!
                 } catch (error: any) {
                     // Don't retry on bad requests or auth errors (4xx)
                     if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
                         bail(new Error(`Non-retriable Plivo error: ${error.statusCode} - ${error.message}`));
                         return; // Bail out definitively
                     }
                     // For other errors (network, 5xx), throw to trigger retry
                     console.warn(`Retrying attempt ${attempt} due to error: ${error.message}`);
                     throw error; // Throw error to signal retry is needed
                 }
             },
             {
                 retries: 3,        // Number of retries
                 factor: 2,         // Exponential factor
                 minTimeout: 1000,  // Initial timeout 1s
                 randomize: true,   // Add jitter
             }
         );
         // ... handle success ...
         console.log("Plivo API Response after retries:", response);
         return NextResponse.json({ /* success response */ }, { status: 200 });
      
      } catch (error: any) {
         // ... handle final error after all retries failed ...
         console.error("Failed to send MMS after multiple retries:", error.message);
         return NextResponse.json({ error: `Failed to send MMS after retries: ${error.message}` }, { status: 500 });
      }
  4. Frontend Error Handling:

    • The handleSubmit function in src/app/page.tsx already catches errors from the fetch call and non-OK responses.
    • It displays user-friendly messages based on the error field from the API response or a generic message. This is good practice.

Frequently Asked Questions About Plivo MMS

What countries does Plivo support for MMS?

Plivo supports MMS only within the US and Canada. You can send MMS messages from US and Canadian long code, toll-free, and short code numbers to major carriers including AT&T, Verizon, T-Mobile, Rogers, Bell, Fido, Telus, and Wind Canada. International MMS outside these two countries is not currently supported.

What media formats does Plivo MMS support?

Plivo MMS supports JPEGs, GIFs, MP3 audio files, and MP4 video files. All media must be accessible via a publicly available URL.

What is the latest version of the Plivo Node.js SDK?

The latest version of the Plivo Node.js SDK is v4.74.0 (as of January 2025). Install it using npm install plivo. The SDK supports TypeScript and can automatically fetch credentials from environment variables.

Do I need to verify phone numbers for trial accounts?

Yes, if using a Plivo trial account, destination phone numbers must be verified (sandboxed) in the Plivo Console under "Phone Numbers" > "Sandbox Numbers" before you can send messages to them.

How do I handle MMS delivery failures?

Implement retry logic with exponential backoff for transient errors (5xx status codes, network errors). Do not retry on 4xx errors (bad requests, invalid numbers). Use the async-retry library and set a maximum of 3 retries with jitter to avoid overwhelming the API.

Can I send MMS from toll-free numbers?

Yes, Plivo supports MMS on toll-free numbers in the US and Canada. Ensure the toll-free number is MMS-enabled when renting it from the Plivo Console.

What is the E.164 format for phone numbers?

E.164 is the international phone number format: +[country code][subscriber number]. For example, a US number would be +14155551234. Plivo requires destination numbers in E.164 format.

How do I secure my Plivo credentials in Next.js?

Store credentials in .env.local without the NEXT_PUBLIC_ prefix to keep them server-side only. Never commit .env.local to version control – add it to .gitignore. Access credentials via process.env in API routes only.

What HTTP methods does the Next.js App Router support?

Next.js App Router route handlers support GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. Export async functions with these names from route.ts files in the app/api/ directory.

How long does it take for an MMS to be delivered?

MMS delivery times vary by carrier but typically occur within seconds to a few minutes. Use Plivo webhooks to receive delivery status updates and track message delivery in real-time.

Database Integration for Message History

For this specific guide (sending one-off MMS via a form), a database is not strictly required. The state is transient – we take user input, send it to Plivo, and report back.

If you needed to:

  • Store message history: Track sent messages, their status (using Plivo webhooks for delivery reports), recipients, etc.
  • Manage contacts: Store recipient information.
  • Queue messages: Implement a more robust queuing system.

You would typically add:

  1. Database Choice: PostgreSQL, MySQL, MongoDB, etc.
  2. ORM/Query Builder: Prisma (recommended for Next.js/TypeScript), TypeORM, Drizzle ORM, or Knex.js.
  3. Schema Example (Using Prisma):
prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Message {
  id            String   @id @default(cuid())
  messageUuid   String   @unique // Plivo's message UUID
  to            String
  from          String
  text          String
  mediaUrl      String?
  status        String   @default("queued") // queued, sent, delivered, failed
  errorMessage  String?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  @@index([status])
  @@index([createdAt])
}

model Contact {
  id          String   @id @default(cuid())
  phoneNumber String   @unique
  name        String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}
  1. Setup Steps:

    • Install Prisma: npm install prisma @prisma/client
    • Initialize: npx prisma init
    • Add DATABASE_URL to .env.local
    • Run migrations: npx prisma migrate dev --name init
    • Generate client: npx prisma generate
  2. Update API Route to Store Messages:

typescript
// src/app/api/send-mms/route.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function POST(request: NextRequest) {
  // ... existing validation code ...

  try {
    const response = await client.messages.create(
      senderId, to, text,
      { type: "mms", media_urls: [mediaUrl] }
    );

    // Store in database
    await prisma.message.create({
      data: {
        messageUuid: response.messageUuid[0],
        to,
        from: senderId,
        text,
        mediaUrl,
        status: 'sent'
      }
    });

    return NextResponse.json({
      success: true,
      message: `MMS queued successfully`,
      message_uuid: response.messageUuid[0]
    }, { status: 200 });

  } catch (error: any) {
    // Log failure to database if needed
    console.error("Error sending MMS:", error);
    return NextResponse.json(
      { error: 'Failed to send MMS.' },
      { status: 500 }
    );
  }
}
  1. Webhook Handler for Delivery Status:
typescript
// src/app/api/webhooks/plivo/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { MessageUUID, Status, ErrorMessage } = body;

    // Update message status in database
    await prisma.message.update({
      where: { messageUuid: MessageUUID },
      data: {
        status: Status.toLowerCase(),
        errorMessage: ErrorMessage || null
      }
    });

    return NextResponse.json({ success: true }, { status: 200 });
  } catch (error) {
    console.error("Webhook error:", error);
    return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
  }
}

Testing and Deployment

Local Testing

  1. Start the Development Server:

    bash
    npm run dev

    Navigate to http://localhost:3000

  2. Test the Form:

    • Enter a verified phone number (E.164 format: +14155551234)
    • Add message text
    • Use the default GIF URL or provide your own
    • Submit and verify the MMS is received
  3. Check Console Logs: Review server-side logs for Plivo API responses and any errors.

Production Deployment

Vercel (Recommended for Next.js):

  1. Prepare for Deployment:

    • Ensure .env.local is in .gitignore
    • Push code to GitHub/GitLab
    • Create Vercel account: vercel.com
  2. Deploy:

    • Import project from Git repository
    • Add environment variables in Vercel dashboard:
      • PLIVO_AUTH_ID
      • PLIVO_AUTH_TOKEN
      • PLIVO_SENDER_ID
      • DATABASE_URL (if using database)
    • Deploy
  3. Post-Deployment:

    • Test production endpoint
    • Configure Plivo webhooks to point to your production URL
    • Monitor logs and error rates

Other Platforms:

  • AWS Amplify: Similar Git-based deployment
  • DigitalOcean App Platform: Node.js application support
  • Railway: PostgreSQL + Next.js hosting
  • Docker: Create Dockerfile for containerized deployment

Security Checklist

  • ✅ Environment variables never committed to Git
  • ✅ API routes validate all inputs
  • ✅ Rate limiting implemented (use @upstash/ratelimit for serverless)
  • ✅ HTTPS enabled in production
  • ✅ Webhook endpoints verify Plivo signatures
  • ✅ Error messages don't expose sensitive data

Conclusion

You've built a production-ready Next.js application for sending MMS messages via the Plivo API. This implementation includes:

  • Secure credential management with environment variables
  • Type-safe TypeScript code
  • Robust error handling and retry logic
  • Input validation for phone numbers and media URLs
  • User-friendly React interface with loading states
  • Optional database integration for message history
  • Deployment-ready architecture

Next Steps:

  • Add authentication (NextAuth.js) to restrict access
  • Implement message queuing with BullMQ or Inngest
  • Add webhook handlers for delivery receipts
  • Create admin dashboard to view message history
  • Add media file upload instead of URL input
  • Implement contact management system

For production applications, consider implementing rate limiting, monitoring with Sentry or LogRocket, and setting up CI/CD pipelines for automated testing and deployment.

Frequently Asked Questions

How to send MMS messages with Next.js?

You can send MMS messages with Next.js by using the Plivo API and Node.js SDK. This involves setting up a Next.js project, installing the Plivo SDK, configuring API credentials, and creating an API route to handle MMS sending logic. The frontend will use a form to collect recipient details, message content, and media URL, then send a POST request to the API route, which interacts with Plivo.

What is the Plivo Node.js SDK used for?

The Plivo Node.js SDK simplifies interaction with the Plivo REST API, making it easier to send MMS messages from your Next.js application. It handles authentication and communication with the Plivo API, allowing you to focus on the application logic rather than low-level API details.

Why use Next.js for sending MMS with Plivo?

Next.js is chosen for its robust features, such as API routes and server-side rendering (SSR), which are beneficial for handling API interactions securely and providing a good developer experience. The API routes simplify backend logic and allow for secure handling of sensitive information, such as API credentials.

When should I verify destination numbers in Plivo?

If you're using a Plivo trial account, you must verify any destination phone numbers before sending MMS messages to them. This is done through the Plivo Console under "Phone Numbers" > "Sandbox Numbers" to ensure compliance with Plivo's trial account restrictions.

Can I use a JavaScript Next.js project with Plivo?

While the guide recommends TypeScript for enhanced type safety, you can use a regular JavaScript Next.js project with Plivo. The core logic and setup will remain similar, with slight differences in type declarations and error handling.

How to set up Plivo credentials in a Next.js project?

Store your Plivo Auth ID, Auth Token, and MMS-enabled Plivo phone number (digits only) as environment variables in a `.env.local` file. This file should be added to your `.gitignore` to avoid committing secrets to version control. Next.js automatically loads these variables server-side via `process.env`.

What is the project structure for a Next.js Plivo MMS sender?

The main parts are `src/app/api/send-mms/route.ts` for API logic, `src/app/page.tsx` for the frontend form, and `.env.local` for storing the credentials securely. The API route handles POST requests containing recipient number, message text, and media URL, sending data to the Plivo API via the Plivo SDK.

How to handle errors when sending Plivo MMS?

The provided code implements error handling for missing credentials, invalid phone numbers, and issues with the Plivo API call. It uses a try-catch block to capture errors from `client.messages.create()` and provides specific feedback to the user, such as a 400 Bad Request for incorrect number formats. Additionally, structured logging can help in managing errors effectively.

What is the role of input validation in Plivo MMS sending?

Input validation is crucial for preventing errors and ensuring the MMS message is sent correctly. The code checks for missing fields (`to`, `text`, `mediaUrl`), validates the format of the destination phone number, and checks for a valid media URL. It returns 400 Bad Request if any issues are found.

How to structure the Plivo API call in Next.js?

The `client.messages.create()` method takes the sender's Plivo number (digits only), recipient number, message text, and an options object. This object includes the message type (`"mms"`) and an array of media URLs (`media_urls`). Ensure your sender's number is MMS-enabled and obtained from the Plivo console.

Why is my Plivo MMS not sending from a trial account?

A common reason is unverified destination numbers. Trial accounts require destination numbers to be added to the Sandbox Numbers list within the Plivo console for testing. Ensure the recipient's number is verified there, and restart your server if you've made changes to the `.env.local` file after starting the Next.js dev server.

How to improve error handling for Plivo MMS in production?

Use a structured logging library like `pino` or `winston` for logging Plivo API responses and errors in a JSON format, making analysis easier. Implement retry mechanisms using libraries like `async-retry` with exponential backoff and jitter for transient errors. Always log the full error details server-side and return a generic `500 Internal Server Error` to the client without revealing sensitive configuration details.