Frameworks and agents · Updated 2026-09-22
Send email from Hono
One POST route that sends a transactional email, the same file on Cloudflare Workers, Deno, Bun and Node.
To send email from a Hono app, add one POST route that calls AgentiSend and returns the message id. The SDK is plain fetch with no Node built-ins, so the same file runs on Cloudflare Workers, Deno, Bun and Node. It is executed against a real API on every build of this site's repository.
Install
pnpm add hono agentisendEnvironment
| Variable | Required | What it is |
|---|---|---|
AGENTISEND_API_KEY | yes | A key with sending_access. |
MAIL_FROM | yes | The From address, on a domain you have verified. |
AGENTISEND_BASE_URL | no | Defaults to https://api.agentisend.com. |
On Workers, read the key from the binding rather than the process environment and pass it to the constructor: new AgentiSend(c.env.AGENTISEND_API_KEY).
The route
/**
* Hono — one POST route that sends a transactional email.
*
* Hono runs on Workers, Deno, Bun and Node, and the AgentiSend SDK is plain
* fetch with no Node built-ins, so this file is the same on all of them.
*/
import { Hono } from 'hono';
import { AgentiSend, AgentiSendError } from 'agentisend';
const agentisend = new AgentiSend();
function mailFrom(): string {
const from = process.env.MAIL_FROM;
if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
return from;
}
export const app = new Hono();
app.post('/send', async (c) => {
const { email, subject, body } = await c.req.json<{
email?: string;
subject?: string;
body?: string;
}>();
if (!email) return c.json({ error: 'email is required' }, 400);
try {
const { id } = await agentisend.emails.send(
{
from: mailFrom(),
to: email,
subject: subject ?? 'Your receipt',
text: body ?? 'Thanks — your receipt is attached to your account.',
},
{ idempotencyKey: `receipt/${email}` },
);
return c.json({ id });
} catch (err) {
if (err instanceof AgentiSendError) {
// `fix` names the endpoint that repairs the call, so a client can act on
// the failure without a human reading prose.
return c.json({ code: err.code, fix: err.fix }, 502);
}
throw err;
}
});
export default app;Try it
curl -X POST localhost:8787/send \
-H 'content-type: application/json' \
-d '{"email":"you@example.com"}'What comes back when it goes wrong
Every refused request carries code, message, fix and docs_url. The route forwards code and fix, so a caller can act on a refusal without a human reading prose. The error catalogue lists every code.