Skip to content
Browse the docs

Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend

Send email with Deno Deploy

A Deno Deploy app whose Deno.serve handler posts one preview notice. Separate from the Deno runtime guide.

To send email with Deno Deploy, start the app with Deno.serve and post JSON to POST /emails from the handler. This is the current Deno Deploy at console.deno.com. Send email from Deno is the runtime on your own machine. The migration guide says the current platform does not run the old standard-library serve import, and that Deno.serve is the entry to deploy.

The environment-variables guide says you add a variable from the app settings, mark it as a secret when it is a key, and choose the Production context, the Development context, or both. Code reads it with Deno.env.get. The handler below checks the address, sends a preview notice with an idempotency key derived from the recipient, and returns the message id or the refusal's code and fix. The suite calls handle against a local AgentiSend server.

Install

No package to add. The handler uses fetch.

Deno.serve(handle);

Create the app at console.deno.com, in an organization, and deploy this file as the entry. The getting-started page is that dashboard path.

Environment

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access. Store it as a secret on the app.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The route

/**
 * Deno Deploy — the current platform, at console.deno.com.
 *
 * The app entry is `Deno.serve(handle)`. Environment variables are read with
 * `Deno.env.get`. This file posts JSON to POST /emails and also runs on Node,
 * which is how the suite executes it: `process.env` is used when `Deno.env`
 * is absent. It is not the Deno runtime guide.
 *
 * 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': `preview/${email}`,
    },
    body: JSON.stringify({
      from: mailFrom(),
      to: email,
      subject: 'Your preview is deployed',
      text: `Preview 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/deno-deploy/handler.ts, run by the test suite against a local AgentiSend server

Try it

curl -X POST https://<your-app>/ \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","note":"preview"}'

The response is {"id":"…"}. GET /emails/{id} is that preview notice.

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. An address with no domain is answered in the handler. The error catalogue lists every code.

Next