Browse the docs

Frameworks and agents · Updated 2026-09-28

Send email from Nuxt

A Nitro server route under server/api that sends one booking confirmation per booking, refusing a malformed address before the API is called.

To send email from a Nuxt app, add a server route under server/api whose handler calls AgentiSend and returns the message id. The route below confirms a booking: it refuses a malformed address before the API is called, and sends once per booking however many times the request is retried. It is executed against a real API on every build of this site's repository, mounted on the same h3 app Nitro builds: one send, and one malformed address refused with nothing sent.

Install

pnpm add agentisend

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

Save it as server/api/send.post.ts. The .post suffix makes Nitro answer only POST /api/send, and everything under server/ stays on the server, key included.

/**
 * Nuxt — `server/api/send.post.ts`, a Nitro server route that confirms a
 * booking.
 *
 * Everything under `server/` runs on the server only, so the API key never
 * reaches the browser. Nuxt auto-imports `defineEventHandler`, `readBody` and
 * `setResponseStatus`; they are imported from `h3` here so the file also runs
 * outside Nuxt, which is how it is tested.
 */
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
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 default defineEventHandler(async (event) => {
  const { email, bookingId } = (await readBody<{ email?: string; bookingId?: string }>(event)) ?? {};
  if (!email || !ADDRESS.test(email) || !bookingId) {
    setResponseStatus(event, 400);
    return { error: 'email and bookingId are required' };
  }

  try {
    const { id } = await agentisend.emails.send(
      {
        from: mailFrom(),
        to: email,
        subject: 'Your booking is confirmed',
        text: `Booking ${bookingId} is confirmed. The details are in your account.`,
      },
      // One booking, one confirmation, however many times the request is retried.
      { idempotencyKey: `booking-confirmed/${bookingId}` },
    );
    return { id };
  } catch (err) {
    if (err instanceof AgentiSendError) {
      setResponseStatus(event, err.status);
      return { code: err.code, fix: err.fix };
    }
    throw err;
  }
});
examples/nuxt/send.post.ts, executed against the live API on every build

Call it from a page

const { id } = await $fetch('/api/send', {
  method: 'POST',
  body: { email: 'you@example.com', bookingId: 'BK-2291' },
});

The key and its budget

Give the app a key of its own rather than the one you signed in with. POST /api-keys with sending_access mints a key that can send and read what it sent, and nothing else. PATCH /limits/keys/{id} puts a ceiling on it with budget_per_period and period; past the ceiling a send is refused with agent_budget_exceeded, and the route answers with that code and its fix instead of retrying.

The idempotency key

Every send carries Idempotency-Key: booking-confirmed/<bookingId>, derived from the booking and never from the moment. A retry after a timeout gets the first send's id back instead of confirming the same booking twice.

Next