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 agentisendEnvironment
| 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
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;
}
});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.