How to Send OTP with Nuxt 3 (2026)
Nuxt 3 OTP login tutorial using StartMessaging. Uses server routes, useRuntimeConfig for secrets, signed cookies and a clean two-step flow.
StartMessaging Team
Engineering
Nuxt 3 + Nitro gives you typed server routes with native cookie helpers — a tidy match for OTP login. This tutorial wires StartMessaging.
Setup
pnpm create nuxt@latest otp-nuxt
cd otp-nuxt && pnpm install
pnpm add zodRuntime Config
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
smApiKey: process.env.SM_API_KEY,
smBaseUrl: 'https://api.startmessaging.com',
},
});Server Routes
// server/api/auth/send.post.ts
import { randomUUID } from 'node:crypto';
export default defineEventHandler(async (event) => {
const cfg = useRuntimeConfig();
const { phoneNumber } = await readBody(event);
const data = await $fetch<{ data: { requestId: string; expiresAt: string }}>(`${cfg.smBaseUrl}/otp/send`, {
method: 'POST',
headers: { 'X-API-Key': cfg.smApiKey, 'Content-Type': 'application/json' },
body: { phoneNumber, idempotencyKey: randomUUID() },
});
setCookie(event, 'otp_req', data.data.requestId, {
httpOnly: true, sameSite: 'lax', secure: true, maxAge: 900,
});
return { expiresAt: data.data.expiresAt };
});// server/api/auth/verify.post.ts
export default defineEventHandler(async (event) => {
const cfg = useRuntimeConfig();
const { otpCode } = await readBody(event);
const requestId = getCookie(event, 'otp_req');
if (!requestId) throw createError({ statusCode: 400, message: 'No active OTP' });
await $fetch(`${cfg.smBaseUrl}/otp/verify`, {
method: 'POST',
headers: { 'X-API-Key': cfg.smApiKey, 'Content-Type': 'application/json' },
body: { requestId, otpCode },
});
deleteCookie(event, 'otp_req');
setCookie(event, 'session', 'verified', { httpOnly: true, sameSite: 'lax', secure: true, maxAge: 1800 });
return { verified: true };
});Auth Composable
// composables/useAuth.ts
export function useAuth() {
return {
sendOtp: (phoneNumber: string) =>
$fetch('/api/auth/send', { method: 'POST', body: { phoneNumber }}),
verifyOtp: (otpCode: string) =>
$fetch('/api/auth/verify', { method: 'POST', body: { otpCode }}),
};
}Login Page
<!-- pages/login.vue -->
<script setup lang="ts">
const { sendOtp, verifyOtp } = useAuth();
const phone = ref('');
const code = ref('');
const stage = ref<'phone' | 'verify'>('phone');
async function handleSend() {
await sendOtp(phone.value); stage.value = 'verify';
}
async function handleVerify() {
await verifyOtp(code.value); navigateTo('/dashboard');
}
</script>
<template>
<form v-if="stage === 'phone'" @submit.prevent="handleSend">
<input v-model="phone" placeholder="+919876543210" />
<button>Send OTP</button>
</form>
<form v-else @submit.prevent="handleVerify">
<input v-model="code" placeholder="482910" />
<button>Verify</button>
</form>
</template>FAQ
Same flow in Next.js, SvelteKit, Remix? See our tutorial library.
Related Articles
SvelteKit OTP tutorial using StartMessaging — uses form actions, server-only modules, hooks for session, and Zod for validation. Production-ready end-to-end flow.
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.
Remix OTP login tutorial using StartMessaging. Uses action functions, server-only utilities, signed cookies for session, and Zod for validation.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.