Skip to content
Browse the docs

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

Send email with Vercel Functions

A Vercel POST function that sends with the Node SDK and replays a retried call for the same recipient.

To send email with Vercel Functions, export a POST function and save it as api/send.ts. Vercel calls that function with a Web Request on every invocation and never ships the file to the browser, so the API key stays in the project environment. The function below sends with the Node SDK, derives the idempotency key from the recipient, and returns the message id. A second invocation of the same recipient replays the first send. The suite runs this file against a local AgentiSend server.

A function that retries on its own is the case the key budget is for. When the key has spent its ceiling, emails.send throws agent_budget_exceeded and the fix says what to change. POST /limits/keys/{id}/kill stops that one key; later sends from it are refused until a person resumes it. The message id is what you store: GET /emails/{id} is the delivery record.

Install

pnpm add agentisend

Environment

Set these on the Vercel project. A variable that exists only in .env on your laptop is absent when the function runs.

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access. Create one in the console or with POST /api-keys.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The route

/**
 * Vercel Functions — the POST the platform calls once per request.
 *
 * Save this file as `api/send.ts`. Vercel invokes the exported function with
 * a Web Request. AGENTISEND_API_KEY and MAIL_FROM are project environment
 * variables. AGENTISEND_BASE_URL is optional and defaults to the public API.
 */
import { AgentiSend, AgentiSendError } from 'agentisend';

const agentisend = new AgentiSend();

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

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

export async function POST(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 });
  }

  try {
    const { id } = await agentisend.emails.send(
      {
        from: mailFrom(),
        to: email,
        subject: 'Your preview is ready',
        text: `Preview note: ${note}`,
      },
      // The recipient, not the clock: a retried invocation replays this send.
      { idempotencyKey: `preview/${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/vercel-functions/handler.ts, run by the test suite against a local AgentiSend server

Try it

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

The response is {"id":"…"}. Call GET /emails/{id} with that id to read the state.

What comes back when it goes wrong

Every refused request carries code, message, fix and docs_url. The function forwards code and fix with the API's status, so the caller can act without a person reading the prose. The error catalogue lists every code. A body that is not JSON, or an address with no domain, is answered here and never reaches POST /emails.

Next