Developer Tutorials

How to Send OTP with Bun (2026)

Bun OTP tutorial using StartMessaging. Uses Bun.serve, Bun.password.hash for credential hygiene, native fetch and zero npm install required.

8 May 20267 min read

StartMessaging Team

Engineering

Bun’s built-in HTTP server, native fetch and zero-config TS make OTP integrations a few-file affair. This tutorial uses StartMessaging.

Setup

bun init -y otp-bun
cd otp-bun
echo 'SM_API_KEY=sm_live_xxx' > .env

Bun.serve OTP App

// src/index.ts
const apiKey = Bun.env.SM_API_KEY!;

async function smSend(phoneNumber: string) {
  const r = await fetch('https://api.startmessaging.com/otp/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
    body: JSON.stringify({ phoneNumber, idempotencyKey: crypto.randomUUID() }),
  });
  if (!r.ok) throw new Error('send failed');
  return (await r.json()).data as { requestId: string; expiresAt: string };
}

async function smVerify(requestId: string, otpCode: string) {
  const r = await fetch('https://api.startmessaging.com/otp/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
    body: JSON.stringify({ requestId, otpCode }),
  });
  return r.ok;
}

Bun.serve({
  port: 3001,
  async fetch(req) {
    const url = new URL(req.url);
    if (req.method === 'POST' && url.pathname === '/auth/send-otp') {
      const { phoneNumber } = await req.json();
      const data = await smSend(phoneNumber);
      return new Response(JSON.stringify({ requestId: data.requestId, expiresAt: data.expiresAt }));
    }
    if (req.method === 'POST' && url.pathname === '/auth/verify-otp') {
      const { requestId, otpCode } = await req.json();
      const ok = await smVerify(requestId, otpCode);
      return new Response(JSON.stringify({ verified: ok }), { status: ok ? 200 : 401 });
    }
    return new Response('Not Found', { status: 404 });
  },
});

Session via Cookie

Bun has no built-in session helper, but signing a cookie withBun.password.hash + a secret works. For production, prefer the iron-session npm module.

Tests

// src/index.test.ts
import { describe, it, expect, mock } from 'bun:test';

it('sends OTP', async () => {
  globalThis.fetch = mock(() => new Response(JSON.stringify({
    data: { requestId: 'req_x', expiresAt: 't', attemptsLeft: 3 }
  }), { status: 200 })) as any;
  const r = await fetch('http://localhost:3001/auth/send-otp', {
    method: 'POST',
    body: JSON.stringify({ phoneNumber: '+919876543210' }),
  });
  expect(r.ok).toBe(true);
});

FAQ

Pair with Hono for a richer framework on top of Bun.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.