Send email with Hono
One handler that runs the same on Node, Bun, Deno and the edge.
You need a verified sending domain and an API key from Settings › API Keys. If neither exists yet, the quickstart covers both in a couple of minutes.
Install
npm i honoSend
import { Hono } from "hono";
const app = new Hono<{ Bindings: { SENDING_API_KEY: string } }>();
app.post("/signup", async (c) => {
const { email, userId } = await c.req.json();
const res = await fetch("https://sending.dev/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${c.env.SENDING_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Acme <[email protected]>",
to: email,
subject: "Welcome",
html: "<p>You're in.</p>",
idempotencyKey: `welcome-${userId}`,
}),
});
return c.json(await res.json(), res.status as 200);
});
export default app;A successful call answers 202 with the message id: accepted and queued, not yet delivered. Delivery, opens, bounces and complaints arrive later, on webhooks or in the dashboard.
What changes in Hono
fetch is the portable path here: the same handler deploys to Workers, Deno Deploy, Bun and Node without swapping the HTTP client.
The thing that catches people
On the edge the key comes from c.env, not process.env. Reading process.env in a Workers deployment gives you undefined and a 401 that looks like a bad key.
Next
- All the fields: cc and bcc, reply-to, attachments, templates, scheduling.
- Webhooks: delivery, bounce and complaint events, signed with HMAC.
- Domains: SPF, DKIM and MAIL FROM, and why sending is refused until they are in place.
- MCP: the same operations as tools, when the one writing the code is an agent.