Frameworks and agents · Published 2026-11-07 · Updated 2026-11-07 · AgentiSend
Send email from Bun or Deno
A Bun or Deno request handler that posts one email with fetch, an idempotency key, and the code and fix of a refusal. No package on either runtime.
To send email from Bun or Deno, handle a Web Request and post JSON to POST /emails with fetch. Both runtimes serve the same Request/Response handler, so the two files below differ only in what they send: the Bun handler posts a receipt and the Deno handler a deploy notice. Each checks the address, sends with an idempotency key derived from the recipient, and returns the message id, or the refusal's code and fix. Both are run by the test suite against a local AgentiSend server.
Install
No package to add on either runtime. The handler uses the built-in fetch, and the only line that differs is how the server starts:
Bun.serve({ port: 3000, fetch: handle });Deno.serve(handle);Deno needs --allow-net for the request to the API and --allow-env for the variables below. Bun grants both by default.
Environment
| 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. |
The route
Bun
/**
* 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 });
}Deno
/**
* Deno — a request handler that sends with fetch.
*
* Deno 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. Deno reads the environment
* through its own `Deno.env` when `process` is absent.
*
* 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': `notice/${email}`,
},
body: JSON.stringify({
from: mailFrom(),
to: email,
subject: 'Deployment finished',
text: `Deploy 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 });
}Try it
Bun listens on port 3000 as configured above, Deno on its default of 8000:
curl -X POST localhost:3000/ \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","note":"first"}'curl -X POST localhost:8000/ \
-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
- Send email with Deno Deploy is the hosted app, not this runtime
- Send email from Hono, which runs on both
- Quickstart