Integration
Send email from Bun with SNDR.
Send transactional email from a Bun server. Native Bun.serve, sub-millisecond cold start, full Web standards Request / Response — the SDK runs unchanged.
Bun ships Node's APIs (node:crypto, fs, etc.) plus a fast native HTTP server (Bun.serve) and a tight Web-standards story. The SDK works without modification — install, instantiate, send. The recipes below use Bun.serve directly for the minimum viable server; pair with Hono or Elysia when you need richer routing.
Setup
Install + configure
bun add @rkiza/sndr# .env (Bun auto-loads .env when present)
SNDR_API_KEY=sndr_live_x9k2...
SNDR_WEBHOOK_SECRET=...Recipes
The patterns you actually need.
Minimal Bun.serve send endpoint
Bun.serve uses Web standards Request / Response. The SDK works exactly as on Node. Pair with a Bun-native router (Hono, Elysia) when you need more than a few routes.
// server.ts import { Sndr, SndrError } from '@rkiza/sndr'; const sndr = new Sndr(Bun.env.SNDR_API_KEY!); Bun.serve({ port: 3000, async fetch(req) { const url = new URL(req.url); if (req.method === 'POST' && url.pathname === '/api/welcome') { const { email, name } = (await req.json()) as { email: string; name: string }; try { const { id } = await sndr.emails.send({ from: 'hello@yourdomain.com', to: email, subject: `Welcome, ${name}`, html: `<p>Thanks for joining us, ${name}.</p>`, }); return Response.json({ id }); } catch (err) { if (err instanceof SndrError) { return Response.json( { error: { code: err.code, message: err.message } }, { status: (err.status >= 400 && err.status <= 599 ? err.status : 502) }, ); } throw err; } } return new Response('Not found', { status: 404 }); }, }); console.log('listening on http://localhost:3000');Webhook receiver
Bun.serve gives you the Web standard Request directly — req.arrayBuffer() reads the raw bytes for HMAC verification before any JSON parse step touches them.
import { verifyWebhookSignature } from '@rkiza/sndr'; import type { WebhookEventPayload } from '@rkiza/sndr'; Bun.serve({ port: 3001, async fetch(req) { if (req.method !== 'POST' || new URL(req.url).pathname !== '/webhooks/sndr') { return new Response('not found', { status: 404 }); } const raw = Buffer.from(await req.arrayBuffer()); const ok = verifyWebhookSignature( raw, req.headers.get('x-sndr-signature') ?? '', Bun.env.SNDR_WEBHOOK_SECRET!, ); if (!ok) return new Response('bad signature', { status: 400 }); const event = JSON.parse(raw.toString('utf8')) as WebhookEventPayload; switch (event.type) { case 'email.delivered': // event.data.email_id, event.data.to: string[] break; case 'email.bounced': // event.data.email_id, event.data.error break; case 'email.unsubscribed': // event.data.addresses: string[] break; } return new Response('ok'); }, });Run sends in a Bun cron / scheduled task
Bun.serve has no scheduling — pair with `bun --watch` for dev, and cron / systemd timers / your platform's scheduler for production. Or use a queue (BullMQ + Bun Redis client).
// scheduled/daily-digest.ts — run via cron: bun run scheduled/daily-digest.ts import { Sndr } from '@rkiza/sndr'; const sndr = new Sndr(Bun.env.SNDR_API_KEY!); const users = await fetchActiveUsers(); await Promise.all( users.map(async (user) => { await sndr.emails.send( { from: 'digest@yourdomain.com', to: user.email, subject: `Your daily digest, ${user.name.split(' ')[0]}`, html: renderDigest(user), }, { idempotencyKey: `daily-digest-${user.id}-${new Date().toISOString().slice(0, 10)}` }, ); }), ); function renderDigest(user: { id: string; name: string }): string { return `<p>Your digest for ${user.name}.</p>`; } async function fetchActiveUsers(): Promise<Array<{ id: string; email: string; name: string }>> { return []; }
Bun FAQ
Does the SDK work with Bun out of the box?
Is Bun.serve fast enough for production?
Should I use Bun + Hono instead of raw Bun.serve?
Send your first Bun email.
3,000 emails a month on the Free plan. No credit card needed.