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.
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
import { randomInt } from 'node:crypto';
const apiKey = Bun.env.SM_API_KEY!;
async function smSend(phoneNumber: string) {
const otp = randomInt(100000, 1000000).toString(); // your server generates the code
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, variables: { otp }, idempotencyKey: crypto.randomUUID() }),
});
if (!r.ok) throw new Error('send failed');
return (await r.json()).data as { otpRequestId: string; messageId: string; status: 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 }), // requestId = otpRequestId from send
});
if (!r.ok) return false;
return (await r.json()).data?.verified === true;
}
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({ otpRequestId: data.otpRequestId }));
}
if (req.method === 'POST' && url.pathname === '/auth/verify-otp') {
const { otpRequestId, otpCode } = await req.json();
const ok = await smVerify(otpRequestId, 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 with
Bun.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({
success: true,
statusCode: 201,
data: { otpRequestId: 'otp_req_x', messageId: 'msg_x', status: 'queued' }
}), { status: 201 })) 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.
StartMessaging Team
StartMessaging Team