Skip to content
Browse the docs

Frameworks and agents · Published 2026-09-29 · Updated 2026-09-30 · AgentiSend

Send email from Bun

A Bun request handler that posts one receipt with fetch, an idempotency key, and the code and fix of a refusal.

To send email from Bun, handle a Web Request and post JSON to POST /emails with fetch. The handler below checks the address, sends with an idempotency key derived from the recipient, and returns the message id, or the refusal's code and fix. It has no extra package. It is run by the test suite against a local AgentiSend server.

Install

No package to add. The handler uses fetch.

Bun.serve({ port: 3000, fetch: handle });

Environment

VariableRequiredWhat 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

/**
 * Bun — a request handler that sends with fetch.
 *
 * Bun runs the same Web `Request` and `fetch` as this file. There is no
 * second package: the handler posts JSON to POST /emails. It also runs on
 * Node, which is how the suite executes it.
 *
 * AGENTISEND_API_KEY, MAIL_FROM, and optionally AGENTISEND_BASE_URL.
 */

function env(name: string): string | undefined {
  const fromProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[
    name
  ];
  const deno = (globalThis as { Deno?: { env: { get(key: string): string | undefined } } }).Deno;
  return fromProcess ?? deno?.env.get(name);
}

function mailFrom(): string {
  const from = env('MAIL_FROM');
  if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
  return from;
}

function baseUrl(): string {
  return (env('AGENTISEND_BASE_URL') ?? 'https://api.agentisend.com').replace(/\/+$/, '');
}

/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function handle(request: Request): 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 });
  }

  const response = await fetch(`${baseUrl()}/emails`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${env('AGENTISEND_API_KEY') ?? ''}`,
      'content-type': 'application/json',
      accept: 'application/json',
      // The recipient, not the moment: a retry of the same note replays.
      'idempotency-key': `receipt/${email}`,
    },
    body: JSON.stringify({
      from: mailFrom(),
      to: email,
      subject: 'Your receipt',
      text: `Receipt note: ${note}`,
    }),
  });
  const parsed = (await response.json()) as { id?: string; error?: { code?: string; fix?: string } };
  if (!response.ok) {
    return Response.json(
      { code: parsed.error?.code, fix: parsed.error?.fix },
      { status: response.status },
    );
  }
  return Response.json({ id: parsed.id });
}
examples/bun/server.ts, run by the test suite against a local AgentiSend server

Try it

curl -X POST localhost:3000/ \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","note":"first"}'

What comes back when it goes wrong

Every refused request carries code, message, fix and docs_url. The handler answers with the API's status and forwards code and fix. The error catalogue lists every code.

Next