Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend
Send email with Cloudflare Workers
A Cloudflare Workers fetch handler that sends with the Node SDK, reading the key from the binding.
To send email with Cloudflare Workers, export a module whose fetch reads the key from the binding and passes it into the SDK. A Worker has no process object unless you turn the nodejs compatibility flag on, and the SDK reads process when the key, the base URL, or the user agent is left out of the constructor. Passing all three keeps the send on the binding you set in Wrangler. The handler returns the message id. The suite runs this file against a local AgentiSend server, supplying the same three values a binding would.
This is the Worker itself. The Hono guide is a route that can be mounted on Workers; it is a different file. After the send, GET /emails/{id}/explain answers why a message was held. PATCH /limits/keys/{id} is how that key's ceiling is changed, and raising it is a person's decision.
Install
pnpm add agentisendThe Worker bundler must include the package. Wrangler does when the import is in the module.
Environment
Add the names as plain-text bindings or as Worker secrets. The script never contains the key.
| Binding | 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. |
The route
/**
* Cloudflare Workers — a module worker that sends one publish notice.
*
* The key, the From address and the base URL are bindings on `env`. They are
* passed into the constructor on purpose: omitted, the SDK reads `process`,
* and a Worker has no `process` object unless the nodejs compatibility flag
* is on. AGENTISEND_BASE_URL is optional.
*/
import { AgentiSend, AgentiSendError } from 'agentisend';
export interface WorkerEnv {
AGENTISEND_API_KEY: string;
MAIL_FROM: string;
AGENTISEND_BASE_URL?: string;
}
/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default {
async fetch(request: Request, env: WorkerEnv): Promise<Response> {
let email = '';
let note = '';
try {
const parsed = (await request.json()) as { email?: string; note?: string };
email = String(parsed.email ?? '').trim();
note = String(parsed.note ?? '');
} catch {
return Response.json({ error: 'email must be one address, like you@example.com' }, { status: 400 });
}
if (!ADDRESS.test(email)) {
return Response.json({ error: 'email must be one address, like you@example.com' }, { status: 400 });
}
if (!env.MAIL_FROM) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
const agentisend = new AgentiSend(env.AGENTISEND_API_KEY, {
baseUrl: env.AGENTISEND_BASE_URL ?? 'https://api.agentisend.com',
userAgent: 'agentisend-worker',
});
try {
const { id } = await agentisend.emails.send(
{
from: env.MAIL_FROM,
to: email,
subject: 'Your worker published',
text: `Publish note: ${note}`,
},
// The recipient, not the clock: a retried fetch replays this send.
{ idempotencyKey: `publish/${email}` },
);
return Response.json({ id });
} catch (err) {
if (err instanceof AgentiSendError) {
return Response.json({ code: err.code, fix: err.fix }, { status: err.status });
}
return Response.json({ error: 'send failed' }, { status: 500 });
}
},
};wrangler.toml names the module and the bindings. A secret is wrangler secret put AGENTISEND_API_KEY.
Try it
curl -X POST https://<your-worker>/ \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","note":"published"}'What comes back when it goes wrong
Every refused request carries code, message, fix and docs_url. The handler returns code and fix with the API's status. The error catalogue lists every code. An address that fails the check in this file is answered before POST /emails.