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

Installbash
bun add @rkiza/sndr
Environmentenv
# .env (Bun auto-loads .env when present)
SNDR_API_KEY=sndr_live_x9k2...
SNDR_WEBHOOK_SECRET=...

Recipes

The patterns you actually need.

  1. 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');
  2. 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');
      },
    });
  3. 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?
Yes. Bun ships node:crypto and full fetch support; no polyfills or compatibility shims required.
Is Bun.serve fast enough for production?
Yes — Bun's HTTP server is one of the fastest in the Node-compatible ecosystem. For SaaS workloads it's overkill but the developer-experience benefits stand: instant cold starts, native TypeScript, integrated test runner.
Should I use Bun + Hono instead of raw Bun.serve?
For anything beyond 3-4 routes, yes. Hono on Bun gives you typed routing, middleware, and the same handler code that works on Workers / Node. See /integrations/hono for the equivalent recipes.

Send your first Bun email.

3,000 emails a month on the Free plan. No credit card needed.