Developer Tutorials

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.

8 May 20268 min read

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 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.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.