Send OTP in Next.js (App Router) — Server Actions Guide 2026
Send and verify SMS OTPs from a Next.js 14/15 App Router app using server actions and the StartMessaging API. Includes a full login form, server actions, and middleware.
StartMessaging Team
Engineering
The Next.js App Router’s server actions make phone OTP login almost trivial: no API routes, no client-side fetch, just a form that calls a server function. This guide builds a complete OTP login flow backed by the StartMessaging OTP API.
Why Server Actions for OTP
Server actions run on the Next.js server, so your StartMessaging API key never reaches the browser. They progressively enhance form submissions, work without JavaScript, and let you set cookies atomically with the response.
Environment Setup
# .env.local
STARTMESSAGING_API_KEY=sm_live_xxxxxxxxxxxxxxxxxxxx
STARTMESSAGING_BASE_URL=https://api.startmessaging.comServer Action to Send OTP
// app/(auth)/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { randomUUID } from 'crypto';
const BASE = process.env.STARTMESSAGING_BASE_URL!;
const KEY = process.env.STARTMESSAGING_API_KEY!;
export async function sendOtpAction(formData: FormData) {
const phone = String(formData.get('phone') ?? '').trim();
if (!/^\+91\d{10}$/.test(phone)) {
return { error: 'Enter a valid Indian mobile number' };
}
const res = await fetch(`${BASE}/otp/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': KEY },
body: JSON.stringify({ phoneNumber: phone, idempotencyKey: randomUUID() }),
cache: 'no-store',
});
const json = await res.json();
if (!res.ok) return { error: json.message ?? 'Failed to send OTP' };
cookies().set('otp_request_id', json.data.requestId, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600,
});
redirect('/login/verify');
}Server Action to Verify OTP
export async function verifyOtpAction(formData: FormData) {
const code = String(formData.get('code') ?? '').trim();
const requestId = cookies().get('otp_request_id')?.value;
if (!requestId) return { error: 'Session expired. Send a new OTP.' };
const res = await fetch(`${BASE}/otp/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': KEY },
body: JSON.stringify({ requestId, otpCode: code }),
cache: 'no-store',
});
const json = await res.json();
if (!res.ok || !json.data?.verified) return { error: 'Wrong code' };
cookies().delete('otp_request_id');
cookies().set('session', 'verified', { httpOnly: true, secure: true, sameSite: 'lax' });
redirect('/dashboard');
}Client Login Form
// app/(auth)/login/page.tsx
import { sendOtpAction } from '../actions';
export default function LoginPage() {
return (
<form action={sendOtpAction} className="space-y-4">
<input name="phone" type="tel" placeholder="+91 98765 43210" required />
<button type="submit">Send OTP</button>
</form>
);
}Middleware to Protect Routes
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith('/dashboard')) {
if (req.cookies.get('session')?.value !== 'verified') {
return NextResponse.redirect(new URL('/login', req.url));
}
}
return NextResponse.next();
}Best Practices
- Use the Web OTP API on the verify page so Chrome on Android can autofill the code. See our guide on OTP autofill on Android & iOS.
- Rate-limit the send action by phone and IP using Vercel KV.
- Encrypt the session cookie with iron-session or jose.
- Show resend timer in the UI to prevent users from spamming send.
FAQ
Curious about pricing? It’s Rs 0.25 per OTP with no DLT registration required.
Related Articles
Step-by-step Node.js tutorial to send and verify OTP via SMS using the StartMessaging API. Includes fetch examples, error handling, and verification flow.
Architecture guide for building a production-ready OTP verification flow covering generation, delivery, verification, retry logic, expiry, and security best practices.
Improve OTP UX with Android SMS Retriever, User Consent API, and iOS one-time code fields. Aligns with TRAI DLT-approved SMS templates and StartMessaging when your backend sends the SMS.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.