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
pnpm add @rkiza/sndr hono
# or
npm install @rkiza/sndr hono
# or with Bun
bun add @rkiza/sndr hono# 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_KEYRecipes
The patterns you actually need.
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 upVerify 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;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;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?
Does the SDK work on Bun?
Should I instantiate Sndr once or per-request?
How do I handle 5xx errors from the API?
Send your first Hono email.
3,000 emails a month on the Free plan. No credit card needed.