code examples

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

Send SMS with Plivo in Next.js and Supabase: Complete Guide

Build a secure SMS API endpoint using Plivo, Next.js 15 App Router, and Supabase authentication. Learn to send text messages programmatically with proper error handling and authentication.

Send SMS with Plivo in Next.js and Supabase: Complete Guide

This guide shows you how to build a secure Next.js API endpoint that sends SMS messages through Plivo, protected by Supabase authentication. You'll create a production-ready /api/send-sms route handler that accepts authenticated requests and delivers text messages programmatically.

By the end, you'll have a functional Next.js application with Supabase user authentication and a protected API route for sending SMS via Plivo.

Project Overview and Goals

Goal: Build a secure Next.js App Router API endpoint that authenticates users with Supabase, then sends SMS messages via the Plivo API.

Problem Solved: Provides authenticated SMS functionality for applications requiring secure, user-specific message delivery without exposing Plivo credentials to clients.

Technologies:

  • Next.js 15: React framework with App Router for building full-stack applications
  • Plivo Messages API: Cloud communication platform for sending SMS, voice, and messaging
  • Plivo Node SDK (plivo): Official Node.js library for Plivo API integration
  • Supabase: Backend-as-a-Service providing authentication, database, and real-time features
  • Supabase Auth: User authentication and session management

System Architecture:

+----------------+ +------------------------+ +---------------+ +-----------+ | Next.js Client |------>| Next.js API Route |------>| Plivo API |------>| User Phone| | (Authenticated)| | /api/send-sms | | (Messages) | | (SMS) | | with Supabase | | Protected by Supabase | | | | | +----------------+ +------------------------+ +---------------+ +-----------+ | | Validates session with: | - Supabase Auth Token | - Server-side auth check

Prerequisites:

  • Node.js 18+: Download from nodejs.org
  • Plivo Account: Sign up at Plivo Console
  • Plivo Auth ID and Auth Token: Find in your Plivo Dashboard
  • Plivo Phone Number: Purchase a phone number with SMS capability
  • Supabase Project: Create at supabase.com
  • Supabase Project URL and Anon Key: Found in your Supabase project settings
  • Basic understanding of Next.js App Router, React, and REST APIs

Final Outcome: A Next.js application with Supabase authentication where logged-in users can send SMS messages through a protected /api/send-sms endpoint powered by Plivo.


1. Setting Up the Project

Initialize your Next.js project with TypeScript and install required dependencies.

  1. Create Next.js Application:

    bash
    npx create-next-app@latest plivo-sms-app

    When prompted, select:

    • TypeScript: Yes
    • ESLint: Yes
    • Tailwind CSS: Yes (optional, for styling)
    • src/ directory: No
    • App Router: Yes
    • Import alias: No (or default @/*)
  2. Navigate to Project:

    bash
    cd plivo-sms-app
  3. Install Dependencies:

    bash
    npm install plivo @supabase/supabase-js @supabase/ssr
    • plivo: Official Plivo Node.js SDK for sending SMS
    • @supabase/supabase-js: Supabase JavaScript client
    • @supabase/ssr: Supabase utilities for server-side rendering and API routes
  4. Create Environment Variables File:

    bash
    touch .env.local
  5. Configure .gitignore:

    Ensure .env.local is listed in .gitignore (Next.js includes this by default):

    plaintext
    # .gitignore
    .env.local
    .env*.local
  6. Project Structure:

    Your project structure should include:

    plivo-sms-app/ ├── app/ │ ├── api/ │ │ └── send-sms/ │ │ └── route.ts # API endpoint for sending SMS │ ├── layout.tsx │ └── page.tsx ├── .env.local # Environment variables (gitignored) ├── .gitignore ├── package.json ├── tsconfig.json └── next.config.js

2. Configuring Environment Variables

Set up your Plivo and Supabase credentials securely using environment variables.

  1. Open .env.local:

    Add your Plivo and Supabase credentials:

    bash
    # .env.local
    
    # Plivo API 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
    
    # Your Plivo phone number (sender ID)
    # Use E.164 format: +12015551234
    PLIVO_FROM_NUMBER=+12015551234
    
    # Supabase Configuration
    # Find these in your Supabase Project Settings -> API
    NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
  2. Security Notes:

    • Never commit .env.local to version control
    • Variables prefixed with NEXT_PUBLIC_ are exposed to the browser – use only for public keys
    • Plivo credentials (Auth ID and Token) are server-side only (no NEXT_PUBLIC_ prefix)
    • Restart your Next.js dev server after changing environment variables

3. Setting Up Supabase Authentication

Create Supabase utility functions for server-side authentication in API routes.

  1. Create Supabase Server Client Utility:

    Create lib/supabase/server.ts:

    typescript
    // lib/supabase/server.ts
    import { createServerClient } from '@supabase/ssr'
    import { cookies } from 'next/headers'
    
    export async function createClient() {
      const cookieStore = await cookies()
    
      return createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            get(name: string) {
              return cookieStore.get(name)?.value
            },
          },
        }
      )
    }

    This creates a Supabase client configured for Next.js App Router server components and route handlers, with automatic cookie management for session persistence.

  2. Create Client-Side Supabase Utility:

    Create lib/supabase/client.ts for client-side authentication:

    typescript
    // lib/supabase/client.ts
    import { createBrowserClient } from '@supabase/ssr'
    
    export function createClient() {
      return createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
      )
    }

4. Building the API Endpoint

Create the protected API route handler for sending SMS messages via Plivo.

  1. Create API Route File:

    Create app/api/send-sms/route.ts:

    typescript
    // app/api/send-sms/route.ts
    import { NextRequest, NextResponse } from 'next/server'
    import plivo from 'plivo'
    import { createClient } from '@/lib/supabase/server'
    
    // Initialize Plivo client
    const plivoClient = new plivo.Client(
      process.env.PLIVO_AUTH_ID!,
      process.env.PLIVO_AUTH_TOKEN!
    )
    
    export async function POST(request: NextRequest) {
      try {
        // 1. Authenticate user with Supabase
        const supabase = await createClient()
        const { data: { user }, error: authError } = await supabase.auth.getUser()
    
        if (authError || !user) {
          return NextResponse.json(
            { success: false, message: 'Unauthorized. Please log in.' },
            { status: 401 }
          )
        }
    
        // 2. Parse and validate request body
        const body = await request.json()
        const { to, text } = body
        const from = process.env.PLIVO_FROM_NUMBER
    
        if (!to || !text) {
          return NextResponse.json(
            {
              success: false,
              message: 'Missing required fields: "to" and "text" are required.'
            },
            { status: 400 }
          )
        }
    
        if (!from) {
          console.error('PLIVO_FROM_NUMBER not configured in environment variables')
          return NextResponse.json(
            { success: false, message: 'Server configuration error.' },
            { status: 500 }
          )
        }
    
        // 3. Validate input types
        if (typeof to !== 'string' || typeof text !== 'string') {
          return NextResponse.json(
            { success: false, message: 'Invalid input types. "to" and "text" must be strings.' },
            { status: 400 }
          )
        }
    
        // 4. Send SMS via Plivo
        console.log(`Sending SMS from ${from} to ${to} for user ${user.email}`)
    
        const response = await plivoClient.messages.create({
          src: from,
          dst: to,
          text: text,
        })
    
        console.log('Plivo API Response:', response)
    
        // 5. Return success response
        return NextResponse.json(
          {
            success: true,
            message: 'SMS sent successfully.',
            messageUuid: response.messageUuid,
            apiId: response.apiId,
          },
          { status: 200 }
        )
    
      } catch (error: any) {
        // Handle errors from Plivo or other sources
        console.error('Error sending SMS:', error)
    
        // Extract meaningful error information
        const errorMessage = error.message || 'An unexpected error occurred while sending SMS.'
        const statusCode = error.statusCode || 500
    
        return NextResponse.json(
          {
            success: false,
            message: `Failed to send SMS: ${errorMessage}`,
            errorDetails: process.env.NODE_ENV === 'development' ? error : undefined
          },
          { status: statusCode }
        )
      }
    }
  2. Code Explanation:

    • Authentication: Validates the user session using supabase.auth.getUser(). Rejects unauthorized requests with 401 status.
    • Input Validation: Checks for required fields (to, text) and validates types.
    • Plivo Integration: Uses plivoClient.messages.create() with src (sender), dst (recipient), and text (message content).
    • Error Handling: Catches exceptions from Plivo API calls and returns appropriate HTTP status codes with error details.
    • Response Format: Returns JSON with success, message, and Plivo response data (messageUuid, apiId).
  3. Phone Number Format (E.164):

    Use E.164 format for all phone numbers: +[country code][number]

    • US: +14155551234
    • UK: +447700900123
    • India: +919876543210
    • Australia: +61412345678

    E.164 format ensures international compatibility and prevents routing errors.


5. Implementing Authentication UI

Create a simple authentication interface for users to log in with Supabase.

  1. Create Login Page:

    Create app/login/page.tsx:

    typescript
    // app/login/page.tsx
    'use client'
    
    import { useState } from 'react'
    import { createClient } from '@/lib/supabase/client'
    import { useRouter } from 'next/navigation'
    
    export default function LoginPage() {
      const [email, setEmail] = useState('')
      const [password, setPassword] = useState('')
      const [loading, setLoading] = useState(false)
      const [message, setMessage] = useState('')
      const router = useRouter()
      const supabase = createClient()
    
      const handleLogin = async (e: React.FormEvent) => {
        e.preventDefault()
        setLoading(true)
        setMessage('')
    
        const { error } = await supabase.auth.signInWithPassword({
          email,
          password,
        })
    
        setLoading(false)
    
        if (error) {
          setMessage(`Error: ${error.message}`)
        } else {
          setMessage('Login successful! Redirecting...')
          router.push('/')
          router.refresh()
        }
      }
    
      return (
        <div style={{ maxWidth: '400px', margin: '100px auto', padding: '20px' }}>
          <h1>Login</h1>
          <form onSubmit={handleLogin}>
            <div style={{ marginBottom: '15px' }}>
              <label>Email:</label>
              <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                required
                style={{ width: '100%', padding: '8px', marginTop: '5px' }}
              />
            </div>
            <div style={{ marginBottom: '15px' }}>
              <label>Password:</label>
              <input
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                required
                style={{ width: '100%', padding: '8px', marginTop: '5px' }}
              />
            </div>
            <button
              type="submit"
              disabled={loading}
              style={{ padding: '10px 20px', cursor: 'pointer' }}
            >
              {loading ? 'Logging in...' : 'Log In'}
            </button>
          </form>
          {message && <p style={{ marginTop: '15px' }}>{message}</p>}
        </div>
      )
    }
  2. Create SMS Sending Interface:

    Update app/page.tsx to create a form for authenticated users:

    typescript
    // app/page.tsx
    'use client'
    
    import { useState, useEffect } from 'react'
    import { createClient } from '@/lib/supabase/client'
    import { useRouter } from 'next/navigation'
    
    export default function Home() {
      const [user, setUser] = useState<any>(null)
      const [to, setTo] = useState('')
      const [text, setText] = useState('')
      const [loading, setLoading] = useState(false)
      const [message, setMessage] = useState('')
      const router = useRouter()
      const supabase = createClient()
    
      useEffect(() => {
        const checkUser = async () => {
          const { data: { user } } = await supabase.auth.getUser()
          setUser(user)
          if (!user) {
            router.push('/login')
          }
        }
        checkUser()
      }, [router])
    
      const handleSendSMS = async (e: React.FormEvent) => {
        e.preventDefault()
        setLoading(true)
        setMessage('')
    
        try {
          const response = await fetch('/api/send-sms', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ to, text }),
          })
    
          const data = await response.json()
    
          if (data.success) {
            setMessage(`✓ SMS sent successfully! Message UUID: ${data.messageUuid}`)
            setTo('')
            setText('')
          } else {
            setMessage(`✗ Error: ${data.message}`)
          }
        } catch (error) {
          setMessage(`✗ Network error: ${error}`)
        } finally {
          setLoading(false)
        }
      }
    
      const handleLogout = async () => {
        await supabase.auth.signOut()
        router.push('/login')
        router.refresh()
      }
    
      if (!user) {
        return <div>Loading...</div>
      }
    
      return (
        <div style={{ maxWidth: '600px', margin: '50px auto', padding: '20px' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
            <h1>Send SMS with Plivo</h1>
            <button onClick={handleLogout} style={{ padding: '8px 16px' }}>
              Log Out
            </button>
          </div>
          <p>Logged in as: {user.email}</p>
    
          <form onSubmit={handleSendSMS} style={{ marginTop: '30px' }}>
            <div style={{ marginBottom: '15px' }}>
              <label>To (Phone Number in E.164 format):</label>
              <input
                type="tel"
                value={to}
                onChange={(e) => setTo(e.target.value)}
                placeholder="+14155551234"
                required
                style={{ width: '100%', padding: '8px', marginTop: '5px' }}
              />
            </div>
            <div style={{ marginBottom: '15px' }}>
              <label>Message:</label>
              <textarea
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="Enter your message here"
                required
                rows={4}
                style={{ width: '100%', padding: '8px', marginTop: '5px' }}
              />
            </div>
            <button
              type="submit"
              disabled={loading}
              style={{ padding: '10px 20px', cursor: 'pointer' }}
            >
              {loading ? 'Sending...' : 'Send SMS'}
            </button>
          </form>
    
          {message && (
            <div style={{
              marginTop: '20px',
              padding: '15px',
              backgroundColor: message.startsWith('✓') ? '#d4edda' : '#f8d7da',
              borderRadius: '4px'
            }}>
              {message}
            </div>
          )}
        </div>
      )
    }

6. Testing the Application

Test your SMS functionality with Supabase authentication.

  1. Create Test User in Supabase:

    • Navigate to your Supabase project dashboard
    • Go to AuthenticationUsers
    • Click Add userCreate new user
    • Enter email and password
    • Confirm the user (or disable email confirmation in AuthenticationSettingsEmail Auth for development)
  2. Start Development Server:

    bash
    npm run dev

    Your application runs at http://localhost:3000

  3. Test Authentication Flow:

    • Navigate to http://localhost:3000
    • You'll be redirected to /login
    • Log in with your test user credentials
    • Upon successful login, you'll return to the home page
  4. Test SMS Sending:

    • Enter a recipient phone number in E.164 format (e.g., +14155551234)
    • Enter a message (e.g., "Test message from Plivo + Next.js")
    • Click Send SMS
    • Check the response message for success or errors
    • Verify the SMS arrives at the recipient's phone
  5. Test with cURL:

    First, get an authentication token by logging in through the UI or using Supabase's API. Then test the endpoint:

    bash
    curl -X POST http://localhost:3000/api/send-sms \
      -H 'Content-Type: application/json' \
      -H 'Cookie: sb-access-token=YOUR_SESSION_TOKEN' \
      -d '{
        "to": "+14155551234",
        "text": "Test message via cURL"
      }'
  6. Expected Success Response:

    json
    {
      "success": true,
      "message": "SMS sent successfully.",
      "messageUuid": "abc123-def456-ghi789",
      "apiId": "xyz-789-456-123"
    }
  7. Expected Error Response (Unauthorized):

    json
    {
      "success": false,
      "message": "Unauthorized. Please log in."
    }

7. Adding Error Handling and Validation

Enhance your API route with comprehensive error handling and input validation.

  1. Phone Number Validation:

    Install a phone number validation library:

    bash
    npm install libphonenumber-js

    Update app/api/send-sms/route.ts to validate phone numbers:

    typescript
    // app/api/send-sms/route.ts (add import at top)
    import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js'
    
    // ... existing code ...
    
    // Add validation before sending SMS (after type validation)
    
    // 4. Validate phone number format
    if (!isValidPhoneNumber(to)) {
      return NextResponse.json(
        {
          success: false,
          message: 'Invalid phone number format. Use E.164 format (+14155551234).'
        },
        { status: 400 }
      )
    }
    
    try {
      const phoneNumber = parsePhoneNumber(to)
      if (!phoneNumber.isValid()) {
        return NextResponse.json(
          { success: false, message: 'Phone number validation failed.' },
          { status: 400 }
        )
      }
    } catch (error) {
      return NextResponse.json(
        { success: false, message: 'Could not parse phone number.' },
        { status: 400 }
      )
    }
    
    // ... continue with sending SMS ...
  2. Message Length Validation:

    Add length checks to prevent oversized messages:

    typescript
    // Add after type validation
    
    // Validate message length (standard SMS is 160 characters for GSM-7, 70 for UCS-2)
    const MAX_SMS_LENGTH = 1600 // Allow up to 10 segments
    
    if (text.length === 0) {
      return NextResponse.json(
        { success: false, message: 'Message text cannot be empty.' },
        { status: 400 }
      )
    }
    
    if (text.length > MAX_SMS_LENGTH) {
      return NextResponse.json(
        {
          success: false,
          message: `Message too long. Maximum ${MAX_SMS_LENGTH} characters allowed. Current: ${text.length}`
        },
        { status: 400 }
      )
    }
  3. Rate Limiting:

    Implement basic rate limiting to prevent abuse. Install @upstash/ratelimit or use Next.js middleware:

    bash
    npm install @upstash/ratelimit @upstash/redis

    Create rate limiter utility in lib/rate-limit.ts:

    typescript
    // lib/rate-limit.ts
    import { Ratelimit } from '@upstash/ratelimit'
    import { Redis } from '@upstash/redis'
    
    // Configure Upstash Redis in your .env.local:
    // UPSTASH_REDIS_REST_URL=...
    // UPSTASH_REDIS_REST_TOKEN=...
    
    export const ratelimit = new Ratelimit({
      redis: Redis.fromEnv(),
      limiter: Ratelimit.slidingWindow(10, '1 h'), // 10 requests per hour per user
      analytics: true,
    })

    Apply rate limiting in your API route:

    typescript
    // app/api/send-sms/route.ts (add import)
    import { ratelimit } from '@/lib/rate-limit'
    
    // Add after authentication check
    
    // Rate limiting (after user authentication)
    const identifier = user.id // Use user ID as rate limit key
    const { success: rateLimitSuccess } = await ratelimit.limit(identifier)
    
    if (!rateLimitSuccess) {
      return NextResponse.json(
        {
          success: false,
          message: 'Rate limit exceeded. Please try again later.'
        },
        { status: 429 }
      )
    }

8. Security Best Practices

Implement security measures to protect your SMS endpoint and credentials.

  1. Environment Variable Security:

    • Never commit .env.local to version control
    • Use different credentials for development, staging, and production
    • Rotate Plivo Auth Token regularly
    • Store production secrets in secure environment variable systems (Vercel, AWS Secrets Manager, etc.)
  2. Input Sanitization:

    While Plivo handles message encoding, sanitize inputs to prevent injection attacks:

    typescript
    // Basic sanitization function
    function sanitizeInput(input: string): string {
      return input
        .trim()
        .replace(/[<>]/g, '') // Remove potential HTML/XML tags
        .substring(0, 1600) // Enforce max length
    }
    
    // Apply before sending
    const sanitizedText = sanitizeInput(text)
  3. CORS Configuration:

    If your API will be called from external domains, configure CORS in app/api/send-sms/route.ts:

    typescript
    export async function OPTIONS(request: NextRequest) {
      return new NextResponse(null, {
        status: 200,
        headers: {
          'Access-Control-Allow-Origin': 'https://your-frontend-domain.com',
          'Access-Control-Allow-Methods': 'POST, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        },
      })
    }
  4. Logging and Monitoring:

    Implement structured logging for debugging and security monitoring:

    typescript
    // Log security-relevant events
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      event: 'sms_sent',
      userId: user.id,
      userEmail: user.email,
      destination: to.substring(0, 6) + '****', // Partially mask phone number
      messageLength: text.length,
      messageUuid: response.messageUuid,
    }))
  5. Message Content Filtering:

    Implement content filtering to prevent abuse:

    typescript
    // Basic spam/abuse detection
    const BLOCKED_KEYWORDS = ['spam', 'scam', 'phishing'] // Extend as needed
    
    function containsBlockedContent(text: string): boolean {
      const lowerText = text.toLowerCase()
      return BLOCKED_KEYWORDS.some(keyword => lowerText.includes(keyword))
    }
    
    if (containsBlockedContent(text)) {
      console.warn(`Blocked message with suspicious content from user ${user.id}`)
      return NextResponse.json(
        { success: false, message: 'Message content not allowed.' },
        { status: 400 }
      )
    }

9. Deploying to Production

Deploy your Next.js application to Vercel or another hosting platform.

  1. Prepare for Deployment:

    • Ensure all environment variables are documented
    • Test thoroughly in development
    • Remove console.log statements or configure proper logging
    • Set up Plivo webhook URLs for delivery receipts (if needed)
  2. Deploy to Vercel:

    bash
    # Install Vercel CLI
    npm i -g vercel
    
    # Deploy
    vercel

    Follow the prompts to link your project and deploy.

  3. Configure Environment Variables in Vercel:

    • Go to your Vercel project dashboard
    • Navigate to SettingsEnvironment Variables
    • Add all variables from .env.local:
      • PLIVO_AUTH_ID
      • PLIVO_AUTH_TOKEN
      • PLIVO_FROM_NUMBER
      • NEXT_PUBLIC_SUPABASE_URL
      • NEXT_PUBLIC_SUPABASE_ANON_KEY
      • UPSTASH_REDIS_REST_URL (if using rate limiting)
      • UPSTASH_REDIS_REST_TOKEN (if using rate limiting)
  4. Configure Supabase for Production:

    • Update Supabase project AuthenticationURL Configuration
    • Add your production domain to Site URL and Redirect URLs
    • Enable email confirmation for production users
  5. Test Production Deployment:

    • Visit your production URL
    • Test authentication flow
    • Send test SMS
    • Monitor Vercel logs for errors
    • Check Plivo dashboard for message delivery status
  6. Set Up Monitoring:

    • Configure Vercel Analytics
    • Set up error tracking (Sentry, LogRocket, etc.)
    • Monitor Plivo usage and costs in Plivo Console
    • Set up alerts for failed authentications or API errors

10. Handling Delivery Receipts and Webhooks

Configure Plivo webhooks to receive delivery status updates for sent messages.

  1. Create Webhook Endpoint:

    Create app/api/webhooks/plivo/route.ts:

    typescript
    // app/api/webhooks/plivo/route.ts
    import { NextRequest, NextResponse } from 'next/server'
    
    export async function POST(request: NextRequest) {
      try {
        const body = await request.json()
    
        console.log('Plivo webhook received:', JSON.stringify(body, null, 2))
    
        // Extract delivery status information
        const {
          MessageUUID,
          To,
          From,
          Status,
          TotalRate,
          TotalAmount,
          Units,
          ErrorCode
        } = body
    
        // Store delivery status in your database
        // await db.updateMessageStatus(MessageUUID, Status)
    
        console.log(`Message ${MessageUUID} status: ${Status}`)
    
        if (ErrorCode) {
          console.error(`Message ${MessageUUID} failed with error: ${ErrorCode}`)
        }
    
        return NextResponse.json({ success: true }, { status: 200 })
    
      } catch (error) {
        console.error('Error processing Plivo webhook:', error)
        return NextResponse.json(
          { success: false, message: 'Webhook processing failed' },
          { status: 500 }
        )
      }
    }
  2. Configure Webhook in Plivo Console:

    • Log into Plivo Console
    • Navigate to MessagingApplications (or use default application settings)
    • Set Message URL to your webhook endpoint: https://your-domain.com/api/webhooks/plivo
    • Set HTTP method to POST
    • Save changes
  3. Webhook Payload Example:

    Plivo sends delivery status updates with this structure:

    json
    {
      "MessageUUID": "abc123-def456-ghi789",
      "From": "+12015551234",
      "To": "+14155556789",
      "Status": "delivered",
      "TotalRate": "0.00650",
      "TotalAmount": "0.00650",
      "Units": 1,
      "MCC": "310",
      "MNC": "260",
      "ErrorCode": null
    }
  4. Delivery Status Values:

    • queued: Message accepted and queued for delivery
    • sent: Message dispatched to carrier
    • delivered: Message delivered to recipient
    • undelivered: Message failed to deliver
    • rejected: Message rejected by carrier
    • failed: Delivery failed
  5. Store Status in Database:

    Extend your application to store message records and update their delivery status. Example Supabase table schema:

    sql
    create table sms_messages (
      id uuid default gen_random_uuid() primary key,
      user_id uuid references auth.users not null,
      message_uuid text unique not null,
      to_number text not null,
      from_number text not null,
      message_text text not null,
      status text default 'queued',
      error_code text,
      created_at timestamp with time zone default timezone('utc'::text, now()) not null,
      updated_at timestamp with time zone default timezone('utc'::text, now()) not null
    );
    
    -- Enable Row Level Security
    alter table sms_messages enable row level security;
    
    -- Users can only view their own messages
    create policy "Users can view own messages"
      on sms_messages for select
      using (auth.uid() = user_id);

11. Advanced Features

Extend your SMS application with additional functionality.

Message Templates

Create reusable message templates:

typescript
// lib/message-templates.ts
export const templates = {
  otp: (code: string) => `Your verification code is: ${code}. Valid for 10 minutes.`,
  welcome: (name: string) => `Welcome to our service, ${name}! We're excited to have you.`,
  reminder: (event: string, time: string) => `Reminder: ${event} at ${time}.`,
}

// Usage in API route
import { templates } from '@/lib/message-templates'

const message = templates.otp('123456')

Bulk SMS Sending

Send messages to multiple recipients:

typescript
// app/api/send-bulk-sms/route.ts
export async function POST(request: NextRequest) {
  // ... authentication ...

  const { recipients, text } = await request.json() // recipients: string[]

  const results = await Promise.allSettled(
    recipients.map(to =>
      plivoClient.messages.create({
        src: process.env.PLIVO_FROM_NUMBER!,
        dst: to,
        text: text,
      })
    )
  )

  const successful = results.filter(r => r.status === 'fulfilled').length
  const failed = results.filter(r => r.status === 'rejected').length

  return NextResponse.json({
    success: true,
    sent: successful,
    failed: failed,
    total: recipients.length
  })
}

Scheduled Messages

Implement scheduled SMS using a job queue or cron:

typescript
// lib/scheduler.ts
import { createClient } from '@supabase/supabase-js'

export async function scheduleMessage(
  userId: string,
  to: string,
  text: string,
  sendAt: Date
) {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for server-side
  )

  const { data, error } = await supabase
    .from('scheduled_messages')
    .insert({
      user_id: userId,
      to_number: to,
      message_text: text,
      send_at: sendAt.toISOString(),
      status: 'scheduled'
    })

  return { data, error }
}

Message History and Analytics

Track sent messages and generate analytics:

typescript
// app/api/messages/history/route.ts
import { createClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const supabase = await createClient()
  const { data: { user }, error: authError } = await supabase.auth.getUser()

  if (authError || !user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { data: messages, error } = await supabase
    .from('sms_messages')
    .select('*')
    .eq('user_id', user.id)
    .order('created_at', { ascending: false })
    .limit(50)

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ messages })
}

12. Troubleshooting Common Issues

Resolve frequent problems when integrating Plivo with Next.js and Supabase.

Issue: "Unauthorized" Error

Cause: Supabase session not found or expired.

Solution:

  • Verify user is logged in through Supabase
  • Check that cookies are enabled in the browser
  • Ensure createClient() is called with correct environment variables
  • Verify Supabase project URL and anon key are correct

Issue: Plivo Authentication Failed

Cause: Invalid PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN.

Solution:

  • Verify credentials in Plivo Console
  • Check for trailing spaces in .env.local
  • Ensure environment variables are loaded (restart dev server)
  • Confirm you're using Auth ID and Auth Token, not API Key

Issue: "Invalid Phone Number" Error

Cause: Phone number not in E.164 format.

Solution:

  • Ensure numbers start with + followed by country code
  • Remove spaces, dashes, or parentheses
  • Use libphonenumber-js to parse and validate
  • Test with: +14155551234 (US), +447700900123 (UK)

Issue: Messages Not Delivered

Cause: Multiple potential causes.

Solution:

  • Check Plivo Console → LogsMessages for delivery status
  • Verify sender number is active and has SMS capability
  • Confirm recipient number is valid and reachable
  • Check for carrier blocks or spam filters
  • Review Plivo account balance and credit

Issue: Rate Limit Errors

Cause: Exceeding Plivo API rate limits or custom rate limits.

Solution:

  • Review Plivo account limits in Console
  • Implement exponential backoff for retries
  • Reduce request frequency
  • Consider upgrading Plivo account for higher limits

Issue: Environment Variables Not Found

Cause: .env.local not loaded or incorrect variable names.

Solution:

  • Restart Next.js development server after changing .env.local
  • Verify variable names match exactly (case-sensitive)
  • Check .env.local is in project root directory
  • Use process.env.VARIABLE_NAME not process.env["VARIABLE_NAME"]

13. Cost Optimization and Best Practices

Minimize Plivo costs and optimize SMS delivery.

Monitor Usage and Costs

  • Check Plivo Console → AccountUsage regularly
  • Set up usage alerts in Plivo Console
  • Track cost per message and total monthly spend
  • Review message delivery rates to identify issues

Reduce Message Costs

  • Optimize message length: Keep under 160 GSM-7 characters (or 70 UCS-2) to avoid multi-part messages
  • Use local numbers: Send from numbers matching recipient country codes
  • Batch processing: Send bulk messages during off-peak hours if possible
  • Validate numbers: Check number validity before sending to avoid wasted attempts

Message Segmentation

SMS messages are charged per segment:

  • GSM-7 encoding: 160 characters per segment
  • UCS-2 encoding: 70 characters per segment (used for Unicode, emojis)
  • Multi-part messages: First segment reduced to 153/67 characters for concatenation header

Calculate segments before sending:

typescript
function calculateSegments(text: string): number {
  const isGSM7 = /^[@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà\n\r]+$/.test(text)

  if (isGSM7) {
    return text.length <= 160 ? 1 : Math.ceil(text.length / 153)
  } else {
    return text.length <= 70 ? 1 : Math.ceil(text.length / 67)
  }
}

// Warn user before sending
const segments = calculateSegments(text)
if (segments > 1) {
  console.warn(`Message will be sent as ${segments} segments`)
}

Implement Delivery Confirmation

Only retry failed messages, not delivered ones:

typescript
// Check delivery status before retry
const status = await checkMessageStatus(messageUuid)
if (status === 'delivered') {
  console.log('Message already delivered, skipping retry')
  return
}

Frequently Asked Questions About Plivo, Next.js, and Supabase SMS Integration

How do I send SMS with Plivo in a Next.js application?

To send SMS with Plivo in Next.js, create an API route using Next.js App Router, install the plivo SDK, and use client.messages.create() with your Auth ID, Auth Token, sender number (src), recipient number (dst), and message text. Authenticate the endpoint with Supabase Auth for secure access.

What's the difference between Plivo and Twilio for Next.js SMS?

Both Plivo and Twilio offer SMS APIs for Next.js applications. Plivo typically offers more competitive international SMS pricing and includes features like voice, messaging, and phone number management. Twilio has broader market adoption but may cost more for high-volume international messaging. Integration patterns are similar – both use REST APIs and official Node.js SDKs.

How do I protect my SMS API endpoint with Supabase authentication?

Protect your Next.js API route by creating a Supabase server client with createServerClient from @supabase/ssr, calling supabase.auth.getUser() to validate the session, and returning a 401 Unauthorized response if no valid user exists. This ensures only authenticated users can send SMS through your endpoint.

Can I use Plivo with Next.js 14 and 15 App Router?

Yes. Plivo works seamlessly with both Next.js 14 and 15 App Router. Create API routes in app/api/send-sms/route.ts, use the POST export function, and integrate the Plivo Node SDK (plivo package). The App Router's server-side nature is ideal for secure API key handling.

What environment variables do I need for Plivo SMS in Next.js?

You need four environment variables: PLIVO_AUTH_ID (your Plivo account ID), PLIVO_AUTH_TOKEN (authentication token), PLIVO_FROM_NUMBER (your Plivo phone number in E.164 format), and optionally Supabase credentials (NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY) for authentication. Store these in .env.local.

How much does it cost to send SMS with Plivo?

Plivo SMS pricing varies by destination country. US SMS typically costs $0.0065–$0.0075 per message segment (160 characters). International rates range from $0.01–$0.15+ per segment depending on the country. Check Plivo Console → Pricing for specific rates. Each 160-character GSM-7 message counts as one segment.

Can I send SMS to international numbers with Plivo and Next.js?

Yes. Use E.164 phone number format with the correct country code (e.g., +447700900123 for UK, +919876543210 for India). Verify your Plivo account has international SMS enabled in Console → Account → Settings. Some countries have additional sender ID requirements or regulatory restrictions.

How do I validate phone numbers before sending SMS in Next.js?

Install libphonenumber-js (npm install libphonenumber-js), import isValidPhoneNumber and parsePhoneNumber, and validate the recipient number before calling Plivo's API. This prevents failed sends due to invalid formats and reduces costs from rejected messages.

What's the best way to handle Plivo webhook delivery receipts in Next.js?

Create a webhook API route at app/api/webhooks/plivo/route.ts, export a POST function to receive delivery status updates, parse the webhook payload (MessageUUID, Status, ErrorCode), and store the status in your Supabase database. Configure the webhook URL in Plivo Console → Messaging → Applications.

Can I use Supabase Row Level Security with SMS message logs?

Yes. Create an sms_messages table in Supabase with a user_id column referencing auth.users, enable Row Level Security, and create a policy that allows users to view only their own messages (auth.uid() = user_id). This provides automatic, database-level authorization for SMS logs.

How do I implement rate limiting for my Plivo SMS endpoint?

Install @upstash/ratelimit and @upstash/redis, create a rate limiter instance with your chosen limits (e.g., 10 requests per hour per user), and check the rate limit in your API route after authenticating the user. Return 429 Too Many Requests if the limit is exceeded.

What's the message length limit for SMS with Plivo?

Standard SMS supports 160 characters using GSM-7 encoding. Messages with Unicode characters (emojis, special characters) use UCS-2 encoding, reducing the limit to 70 characters per segment. Longer messages automatically split into multiple segments, with each segment counted separately for billing.


Summary

You've built a complete, production-ready SMS application using:

  • Plivo API: Cloud messaging platform for sending SMS
  • Next.js 15 App Router: Modern React framework with server-side API routes
  • Supabase Authentication: User authentication and session management
  • TypeScript: Type-safe development
  • Security best practices: Rate limiting, input validation, error handling

Key Features Implemented:

✓ Secure user authentication with Supabase ✓ Protected API endpoint for sending SMS ✓ Phone number validation with E.164 format ✓ Comprehensive error handling ✓ Rate limiting to prevent abuse ✓ Delivery receipt webhooks ✓ Production deployment ready

Next Steps:

  • Implement message templates for common use cases
  • Add SMS analytics and reporting dashboard
  • Build automated messaging workflows
  • Integrate two-factor authentication (2FA) using SMS
  • Explore Plivo's voice and WhatsApp APIs

Frequently Asked Questions

What is the importance of E.164 number format in Vonage SMS?

E.164 is an international standard phone number format that includes the country code, ensuring unambiguous identification of the recipient's number for successful delivery.

How to send SMS with Node.js and Express?

Use the Vonage Messages API with the Express.js framework and Node.js. Set up an Express server, install the Vonage Server SDK, configure API credentials, create a '/send-sms' endpoint, and handle the request to send SMS messages programmatically.

What is Vonage Messages API used for in Node.js?

The Vonage Messages API allows Node.js applications to send SMS messages, as well as other types of messages like MMS and WhatsApp messages. It simplifies the process of integrating communication features into your application.

Why use dotenv with Vonage and Node.js?

Dotenv helps manage environment variables securely in your Node.js projects, including sensitive Vonage API credentials. This avoids hardcoding keys in your code, which is essential for security best practices.

When should I use ngrok with Vonage Messages API?

Ngrok is useful during development when testing Vonage's status webhooks locally, as it exposes your local server to the internet so Vonage can send webhook notifications.

Can I send SMS to international numbers using Vonage?

Yes, you can send SMS to international numbers with Vonage, but ensure your account is enabled for international messaging and you are following country-specific regulations. Use E.164 number formatting.

What is the Vonage Application ID and where to find it?

The Vonage Application ID is a unique identifier for your Vonage application, required to initialize the Vonage SDK. You can find it in your Vonage Dashboard under Applications -> Your Application.

How do I whitelist a number for testing SMS with Vonage?

If on a trial account, you'll need to add numbers under 'Numbers' -> 'Test numbers' to send test SMS messages to them from your Vonage virtual number

How to handle Vonage Messages API errors in Node.js?

Implement a try...catch block around the `vonage.messages.send()` call to handle potential errors during the API request. Return appropriate HTTP status codes and JSON error messages based on the error type and provide detailed logging for debugging.

What is the role of private key in Vonage API integration?

The private key is a crucial security credential for authenticating your Node.js application with the Vonage API. It must be kept secure and never committed to version control.

How to implement rate limiting for Vonage SMS endpoint?

Use middleware like 'express-rate-limit' to control the number of SMS requests from an IP address within a time window. Configure this middleware in your Express app to prevent abuse.

How to set up Vonage API credentials in Node.js?

Store Vonage API credentials (Application ID, Private Key Path, and virtual number) as environment variables in a '.env' file. Load these variables into your Node.js application using the 'dotenv' module. Never expose these credentials in code or version control.

How to receive SMS delivery reports with Vonage Messages API?

Configure the 'Status URL' in your Vonage Application settings. This webhook URL will receive delivery status updates. Create an endpoint to handle these webhooks.