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.
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
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.
Set up your Plivo and Supabase credentials securely using environment variables.
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
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.
This creates a Supabase client configured for Next.js App Router server components and route handlers, with automatic cookie management for session persistence.
Create Client-Side Supabase Utility:
Create lib/supabase/client.ts for client-side authentication:
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 uniquenot 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 timestampwithtime zone default timezone('utc'::text, now()) not null,
updated_at timestampwithtime zone default timezone('utc'::text, now()) not null
);
-- Enable Row Level Securityalter table sms_messages enable row level security;
-- Users can only view their own messagescreate policy "Users can view own messages"
on sms_messages forselectusing (auth.uid() = user_id);
11. Advanced Features
Extend your SMS application with additional functionality.
Message Templates
Create reusable message templates:
TypeScript
// lib/message-templates.tsexportconst 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 routeimport { templates } from'@/lib/message-templates'const message = templates.otp('123456')
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 → Logs → Messages 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 → Account → Usage 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
functioncalculateSegments(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 sendingconst 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 retryconst status = awaitcheckMessageStatus(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
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
Configure the 'Status URL' in your Vonage Application settings. This webhook URL will receive delivery status updates. Create an endpoint to handle these webhooks.
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-smsroute 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:
plivo): Official Node.js library for Plivo API integrationSystem Architecture:
Prerequisites:
Final Outcome: A Next.js application with Supabase authentication where logged-in users can send SMS messages through a protected
/api/send-smsendpoint powered by Plivo.1. Setting Up the Project
Initialize your Next.js project with TypeScript and install required dependencies.
Create Next.js Application:
When prompted, select:
src/directory: No@/*)Navigate to Project:
cd plivo-sms-appInstall Dependencies:
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 routesCreate Environment Variables File:
touch .env.localConfigure
.gitignore:Ensure
.env.localis listed in.gitignore(Next.js includes this by default):Project Structure:
Your project structure should include:
2. Configuring Environment Variables
Set up your Plivo and Supabase credentials securely using environment variables.
Open
.env.local:Add your Plivo and Supabase credentials:
# .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-keySecurity Notes:
.env.localto version controlNEXT_PUBLIC_are exposed to the browser – use only for public keysNEXT_PUBLIC_prefix)3. Setting Up Supabase Authentication
Create Supabase utility functions for server-side authentication in API routes.
Create Supabase Server Client Utility:
Create
lib/supabase/server.ts:// 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.
Create Client-Side Supabase Utility:
Create
lib/supabase/client.tsfor client-side authentication:// 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.
Create API Route File:
Create
app/api/send-sms/route.ts:// 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 } ) } }Code Explanation:
supabase.auth.getUser(). Rejects unauthorized requests with 401 status.to,text) and validates types.plivoClient.messages.create()withsrc(sender),dst(recipient), andtext(message content).success,message, and Plivo response data (messageUuid,apiId).Phone Number Format (E.164):
Use E.164 format for all phone numbers:
+[country code][number]+14155551234+447700900123+919876543210+61412345678E.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.
Create Login Page:
Create
app/login/page.tsx:// 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> ) }Create SMS Sending Interface:
Update
app/page.tsxto create a form for authenticated users:// 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.
Create Test User in Supabase:
Start Development Server:
Your application runs at
http://localhost:3000Test Authentication Flow:
http://localhost:3000/loginTest SMS Sending:
+14155551234)Test with cURL:
First, get an authentication token by logging in through the UI or using Supabase's API. Then test the endpoint:
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" }'Expected Success Response:
{ "success": true, "message": "SMS sent successfully.", "messageUuid": "abc123-def456-ghi789", "apiId": "xyz-789-456-123" }Expected Error Response (Unauthorized):
{ "success": false, "message": "Unauthorized. Please log in." }7. Adding Error Handling and Validation
Enhance your API route with comprehensive error handling and input validation.
Phone Number Validation:
Install a phone number validation library:
Update
app/api/send-sms/route.tsto validate phone numbers:// 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 ...Message Length Validation:
Add length checks to prevent oversized messages:
// 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 } ) }Rate Limiting:
Implement basic rate limiting to prevent abuse. Install
@upstash/ratelimitor use Next.js middleware:Create rate limiter utility in
lib/rate-limit.ts:// 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:
// 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.
Environment Variable Security:
.env.localto version controlInput Sanitization:
While Plivo handles message encoding, sanitize inputs to prevent injection attacks:
// 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)CORS Configuration:
If your API will be called from external domains, configure CORS in
app/api/send-sms/route.ts: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', }, }) }Logging and Monitoring:
Implement structured logging for debugging and security monitoring:
// 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, }))Message Content Filtering:
Implement content filtering to prevent abuse:
// 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.
Prepare for Deployment:
Deploy to Vercel:
# Install Vercel CLI npm i -g vercel # Deploy vercelFollow the prompts to link your project and deploy.
Configure Environment Variables in Vercel:
.env.local:PLIVO_AUTH_IDPLIVO_AUTH_TOKENPLIVO_FROM_NUMBERNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYUPSTASH_REDIS_REST_URL(if using rate limiting)UPSTASH_REDIS_REST_TOKEN(if using rate limiting)Configure Supabase for Production:
Test Production Deployment:
Set Up Monitoring:
10. Handling Delivery Receipts and Webhooks
Configure Plivo webhooks to receive delivery status updates for sent messages.
Create Webhook Endpoint:
Create
app/api/webhooks/plivo/route.ts:// 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 } ) } }Configure Webhook in Plivo Console:
https://your-domain.com/api/webhooks/plivoWebhook Payload Example:
Plivo sends delivery status updates with this structure:
{ "MessageUUID": "abc123-def456-ghi789", "From": "+12015551234", "To": "+14155556789", "Status": "delivered", "TotalRate": "0.00650", "TotalAmount": "0.00650", "Units": 1, "MCC": "310", "MNC": "260", "ErrorCode": null }Delivery Status Values:
queued: Message accepted and queued for deliverysent: Message dispatched to carrierdelivered: Message delivered to recipientundelivered: Message failed to deliverrejected: Message rejected by carrierfailed: Delivery failedStore Status in Database:
Extend your application to store message records and update their delivery status. Example Supabase table schema:
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:
// 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:
// 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:
// 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:
// 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:
createClient()is called with correct environment variablesIssue: Plivo Authentication Failed
Cause: Invalid
PLIVO_AUTH_IDorPLIVO_AUTH_TOKEN.Solution:
.env.localIssue: "Invalid Phone Number" Error
Cause: Phone number not in E.164 format.
Solution:
+followed by country codelibphonenumber-jsto parse and validate+14155551234(US),+447700900123(UK)Issue: Messages Not Delivered
Cause: Multiple potential causes.
Solution:
Issue: Rate Limit Errors
Cause: Exceeding Plivo API rate limits or custom rate limits.
Solution:
Issue: Environment Variables Not Found
Cause:
.env.localnot loaded or incorrect variable names.Solution:
.env.local.env.localis in project root directoryprocess.env.VARIABLE_NAMEnotprocess.env["VARIABLE_NAME"]13. Cost Optimization and Best Practices
Minimize Plivo costs and optimize SMS delivery.
Monitor Usage and Costs
Reduce Message Costs
Message Segmentation
SMS messages are charged per segment:
Calculate segments before sending:
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:
// 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
plivoSDK, and useclient.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
createServerClientfrom@supabase/ssr, callingsupabase.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 thePOSTexport function, and integrate the Plivo Node SDK (plivopackage). 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_URLandNEXT_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.,
+447700900123for UK,+919876543210for 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), importisValidPhoneNumberandparsePhoneNumber, 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 aPOSTfunction 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_messagestable in Supabase with auser_idcolumn referencingauth.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/ratelimitand@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:
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: