Skip to content
Browse the docs

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 agentisend

The 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.

BindingRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults 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 });
    }
  },
};
examples/cloudflare-workers/worker.ts, run by the test suite against a local AgentiSend server

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.

Next