How to Send OTP with Remix (2026)
Remix OTP login tutorial using StartMessaging. Uses action functions, server-only utilities, signed cookies for session, and Zod for validation.
StartMessaging Team
Engineering
Remix actions + server-only utilities are a clean fit for OTP login. This tutorial wires up StartMessaging with Zod validation and a signed-cookie session.
Project Setup
pnpm create remix@latest otp-remix
cd otp-remix && pnpm install
pnpm add zodEnvironment Variables
# .env
SM_API_KEY=sm_live_xxxxxxxxxxxxxxxx
SM_BASE_URL=https://api.startmessaging.com
SESSION_SECRET=replace-meServer-Only StartMessaging Utility
// app/lib/sm.server.ts
import { randomUUID } from 'node:crypto';
export async function smSend(phoneNumber: string) {
const res = await fetch(`${process.env.SM_BASE_URL}/otp/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SM_API_KEY! },
body: JSON.stringify({ phoneNumber, idempotencyKey: randomUUID() }),
});
if (!res.ok) throw new Error((await res.json()).message ?? 'OTP send failed');
return (await res.json()).data as { requestId: string; expiresAt: string };
}
export async function smVerify(requestId: string, otpCode: string) {
const res = await fetch(`${process.env.SM_BASE_URL}/otp/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SM_API_KEY! },
body: JSON.stringify({ requestId, otpCode }),
});
if (!res.ok) throw new Error('Verification failed');
return true;
}Action: Send and Verify
// app/routes/login.tsx
import { type ActionFunctionArgs, json, redirect } from '@remix-run/node';
import { z } from 'zod';
import { smSend, smVerify } from '~/lib/sm.server';
import { commitSession, getSession } from '~/lib/session.server';
export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request);
const form = await request.formData();
const intent = form.get('intent');
if (intent === 'send') {
const phone = z.string().regex(/^\+91\d{10}$/).parse(form.get('phoneNumber'));
const { requestId } = await smSend(phone);
session.set('otpReq', requestId);
return json({ stage: 'verify' }, { headers: { 'Set-Cookie': await commitSession(session) }});
}
if (intent === 'verify') {
const code = z.string().regex(/^\d{4,8}$/).parse(form.get('otpCode'));
const requestId = session.get('otpReq');
if (!requestId) return json({ error: 'No active OTP' });
await smVerify(requestId, code);
session.unset('otpReq');
session.set('userPhone', 'verified');
return redirect('/dashboard', { headers: { 'Set-Cookie': await commitSession(session) }});
}
}Session via Signed Cookie
// app/lib/session.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
const { getSession: gs, commitSession, destroySession } =
createCookieSessionStorage({
cookie: {
name: 'session',
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
secrets: [process.env.SESSION_SECRET!],
maxAge: 30 * 60,
},
});
export const getSession = (request: Request) =>
gs(request.headers.get('Cookie'));
export { commitSession, destroySession };The Form Component
// app/routes/login.tsx (default export)
import { Form, useActionData } from '@remix-run/react';
export default function Login() {
const data = useActionData<typeof action>();
return (
<>
<Form method="post">
<input name="phoneNumber" placeholder="+919876543210" />
<button name="intent" value="send">Send OTP</button>
</Form>
{data?.stage === 'verify' && (
<Form method="post">
<input name="otpCode" placeholder="482910" />
<button name="intent" value="verify">Verify</button>
</Form>
)}
</>
);
}FAQ
Same flow in Next.js, SvelteKit and Express? See our tutorials library.
Related Articles
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.
SvelteKit OTP tutorial using StartMessaging — uses form actions, server-only modules, hooks for session, and Zod for validation. Production-ready end-to-end flow.
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.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.