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.
StartMessaging Team
Engineering
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 zodEnvironment
# .dev.vars (Cloudflare)
SM_API_KEY=sm_live_xxxxxxxxxxxxxxxxThe 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.
Related Articles
Cloudflare Workers OTP tutorial using StartMessaging. Uses Workers fetch, KV for rate-limit, signed-cookie session, and Durable Objects for production-grade pumping defence.
Bun OTP tutorial using StartMessaging. Uses Bun.serve, Bun.password.hash for credential hygiene, native fetch and zero npm install required.
Deno OTP tutorial using StartMessaging. Uses Deno.serve, native fetch, signed-cookie helpers and runs on Deno Deploy with zero infrastructure.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.