Integration

Send email from Hono with SNDR.

Send transactional email from a Hono app — typed routing, middleware, webhook receivers. Use Node-compatible runtimes; Cloudflare Workers requires nodejs_compat.

Hono's small surface — c.req for the request, c.json/c.text for the response, typed bindings for env vars on Workers — is a clean fit for transactional email. The SDK requires Node-compatible crypto and Buffer in addition to fetch. Enable nodejs_compat on Workers; use the HTTP API for targets without Node compatibility. The recipes below cover the four patterns: a typed POST endpoint, a webhook receiver, an OTP flow, and a Cloudflare Workers configuration with typed env bindings.

Setup

Install + configure

Installbash
pnpm add @rkiza/sndr hono
# or
npm install @rkiza/sndr hono
# or with Bun
bun add @rkiza/sndr hono
Environmentenv
# Node / Bun — .env
SNDR_API_KEY=sndr_live_x9k2...
SNDR_WEBHOOK_SECRET=...

# Cloudflare Workers — wrangler.toml
# compatibility_flags = ["nodejs_compat"]
# [vars]
# SNDR_API_KEY = "sndr_live_x9k2..."
# SNDR_WEBHOOK_SECRET = "..."
# Or for secrets: wrangler secret put SNDR_API_KEY

Recipes

The patterns you actually need.

  1. Send from a typed POST handler

    Hono validates JSON bodies via @hono/zod-validator when you add it; without it, c.req.json() returns unknown — narrow with your own type guards or Zod.

    // src/index.ts (Node or Bun)
    import { Hono } from 'hono';
    import { Sndr, SndrError } from '@rkiza/sndr';
    
    const sndr = new Sndr(process.env.SNDR_API_KEY!);
    const app = new Hono();
    
    app.post('/api/welcome', async (c) => {
      const { email, name } = (await c.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 c.json({ id });
      } catch (err) {
        if (err instanceof SndrError) {
          return c.json(
            { error: { code: err.code, message: err.message } },
            err.status as 400 | 401 | 403 | 404 | 422 | 429 | 500,
          );
        }
        throw err;
      }
    });
    
    export default app; // Node/Bun adapters pick this up
  2. Verify an incoming webhook

    Hono's c.req.raw exposes the underlying Web Request — call .arrayBuffer() to read the raw bytes before any JSON parse step consumes the stream.

    import { Hono } from 'hono';
    import { verifyWebhookSignature } from '@rkiza/sndr';
    import type { WebhookEventPayload } from '@rkiza/sndr';
    
    const app = new Hono();
    
    app.post('/webhooks/sndr', async (c) => {
      const raw = Buffer.from(await c.req.raw.arrayBuffer());
      const ok = verifyWebhookSignature(
        raw,
        c.req.header('x-sndr-signature') ?? '',
        process.env.SNDR_WEBHOOK_SECRET!,
      );
      if (!ok) return c.text('bad signature', 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 c.text('ok');
    });
    
    export default app;
  3. Cloudflare Workers — typed env bindings

    On Workers, env vars and secrets live on the Bindings type. Instantiate Sndr per-request because each request gets its own env. The SDK is small (~6 KB gzipped) so the per-request cost is negligible.

    import { Hono } from 'hono';
    import { Sndr } from '@rkiza/sndr';
    
    type Bindings = {
      SNDR_API_KEY: string;
      SNDR_WEBHOOK_SECRET: string;
    };
    
    const app = new Hono<{ Bindings: Bindings }>();
    
    app.post('/api/welcome', async (c) => {
      const sndr = new Sndr(c.env.SNDR_API_KEY);
      const { email, name } = (await c.req.json()) as { email: string; name: string };
      const { id } = await sndr.emails.send({
        from: 'hello@yourdomain.com',
        to: email,
        subject: `Welcome, ${name}`,
        html: `<p>Thanks for joining us, ${name}.</p>`,
      });
      return c.json({ id });
    });
    
    export default app;
  4. OTP flow with KV-backed storage

    On Workers, store the (email → code) pair in KV with a short TTL. On Node, swap KV for Redis. The handler shape is identical because Hono abstracts the runtime.

    import { Hono } from 'hono';
    import { Sndr } from '@rkiza/sndr';
    
    type Bindings = {
      SNDR_API_KEY: string;
      OTP: KVNamespace;
    };
    
    const app = new Hono<{ Bindings: Bindings }>();
    
    app.post('/api/otp/send', async (c) => {
      const sndr = new Sndr(c.env.SNDR_API_KEY);
      const { email } = (await c.req.json()) as { email: string };
      const code = String(Math.floor(100000 + Math.random() * 900000));
      // KV value TTL is in seconds. 10-min lifetime.
      await c.env.OTP.put(`otp:${email}`, code, { expirationTtl: 600 });
    
      await sndr.emails.send({
        from: 'auth@yourdomain.com',
        to: email,
        subject: `Your sign-in code: ${code}`,
        text: `Your code is ${code}. It expires in 10 minutes.`,
      });
    
      return c.json({ ok: true });
    });

Hono FAQ

Does the SDK work on Cloudflare Workers?
Enable nodejs_compat for the entire SDK on Workers. Put compatibility_flags = ["nodejs_compat"] at the top level of wrangler.toml, not under [vars].
Does the SDK work on Bun?
Yes — Bun ships a node:crypto polyfill and full fetch support. The SDK works unchanged.
Should I instantiate Sndr once or per-request?
Per-process is fine on Node/Bun (one HttpClient). On Workers each request gets its own env, so instantiate per-request — the SDK is small enough that the cost is negligible (~0.1ms).
How do I handle 5xx errors from the API?
The SDK auto-retries 429 / 500 / 502 / 503 / 504 with exponential backoff up to 3 attempts by default. Override via the retries config in the Sndr constructor — pass false to disable.

Send your first Hono email.

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