How to Send OTP with Hono (2026)
Hono OTP tutorial using StartMessaging. Targets Cloudflare Workers, Bun and Node. Uses zValidator, JSON schema, signed-cookie session and middleware-based rate limiting.
Hono is the framework of choice for runtime-portable JS APIs. The same code runs on Cloudflare Workers, Bun, Node, Deno or Lambda. OTP login works just as cleanly.
Setup
pnpm create hono@latest otp-hono
cd otp-hono && pnpm install
pnpm add @hono/zod-validator zod
Environment
# .dev.vars (Cloudflare)
SM_API_KEY=sm_live_xxxxxxxxxxxxxxxx
The Hono App
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
type Env = { Bindings: { SM_API_KEY: string; SESSION_SECRET: string } };
const app = new Hono<Env>();
const PhoneSchema = z.object({ phoneNumber: z.string().regex(/^\\+91\\d{10}$/) });
const CodeSchema = z.object({ otpCode: z.string().regex(/^\\d{4,8}$/) });
app.post('/auth/send-otp', zValidator('json', PhoneSchema), async (c) => {
const { phoneNumber } = c.req.valid('json');
const res = await fetch('https://api.startmessaging.com/otp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': c.env.SM_API_KEY },
body: JSON.stringify({ phoneNumber, idempotencyKey: crypto.randomUUID() }),
});
if (!res.ok) return c.json({ error: 'send failed' }, 502);
const { data } = await res.json<any>();
await setSignedCookie(c, 'otp_req', data.requestId, c.env.SESSION_SECRET, {
httpOnly: true, secure: true, sameSite: 'Lax', maxAge: 900,
});
return c.json({ expiresAt: data.expiresAt });
});
app.post('/auth/verify-otp', zValidator('json', CodeSchema), async (c) => {
const { otpCode } = c.req.valid('json');
const requestId = await getSignedCookie(c, c.env.SESSION_SECRET, 'otp_req');
if (!requestId) return c.json({ error: 'no active otp' }, 400);
const res = await fetch('https://api.startmessaging.com/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': c.env.SM_API_KEY },
body: JSON.stringify({ requestId, otpCode }),
});
if (!res.ok) return c.json({ error: 'verify failed' }, 401);
return c.json({ verified: true });
});
export default app;
Rate Limit Middleware
On Cloudflare, use a Durable Object or KV for per-phone counters. On
Node / Bun, an in-memory Map works for a single
process.
Deploy Targets
- Cloudflare Workers:
npx wrangler deploy - Bun:
bun run src/index.ts - Node:
tsx src/index.ts - Deno:
deno run --allow-net src/index.ts
FAQ
Pair this with our Workers guide for the deployment side.
StartMessaging Team
StartMessaging Team