Skip to content

Send from a client · Published 2026-09-22 · Updated 2026-09-29 · AgentiSend

Send email from a Bolt app

To send email from a Bolt app, verify a domain, create a key with a budget, store it as a project secret, and ask Bolt to add a server-side send.

To send email from a Bolt app, verify a domain, create a key with sending_access and a budget, store it as a project secret, and ask Bolt to add a server-side send that calls the API. Bolt has no AgentiSend connector, and this page does not describe one. The sample below is a Next.js route handler, not an edge function and not a client component. If the app Bolt generated is not a Next.js project, ask for the same send in whatever server code it has: the rule that carries over is that the key stays on the server.

Keys and secrets first

Verify the sending domain, then create a key with POST /api-keys and permission sending_access, and set its ceiling with PATCH /limits/keys/{id}. Store two project secrets, which the server reads as environment variables: AGENTISEND_API_KEY for the key and MAIL_FROM for a From address on the verified domain.

The prompt to paste

Add a server-side email send to this app. If it is a Next.js App Router project, create app/api/send/route.ts matching the route sample at https://agentisend.com/docs/guides/nextjs; otherwise add one POST route to the app's own server code. Install the npm package agentisend and read the contract at https://agentisend.com/openapi.json. There is no native AgentiSend connector to enable. Read the key from the project secret AGENTISEND_API_KEY and the sender from MAIL_FROM, and keep both out of client code. Derive the idempotency key from the recipient, return the message id, and save that id with the user it was sent to. On a refusal, return code and fix, and do not retry when retryable is false.

The code that prompt should produce

/**
 * app/api/send/route.ts — a Next.js App Router route handler.
 *
 * Runs on the server only, so the API key never reaches the browser. The
 * handler returns the message id, which is what you store against the user
 * row: every later question ("did it arrive?", "why was it held?") is answered
 * by GET /emails/:id and GET /emails/:id/explain with that id.
 */
import { AgentiSend, AgentiSendError } from 'agentisend';

const agentisend = new AgentiSend();

/** The verified sender. Failing here beats failing at the API with a 422. */
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;
}

export const runtime = 'nodejs';

export async function POST(request: Request): Promise<Response> {
  const { email, name } = (await request.json()) as { email?: string; name?: string };
  if (!email) {
    return Response.json({ error: 'email is required' }, { status: 400 });
  }

  try {
    const { id } = await agentisend.emails.send(
      {
        from: mailFrom(),
        to: email,
        subject: 'Welcome',
        html: `<p>Hello ${name ?? 'there'} — your account is ready.</p>`,
      },
      // Derived from the user, not from the moment: a retried request after a
      // timeout replays the first send instead of mailing them twice.
      { idempotencyKey: `welcome/${email}` },
    );
    return Response.json({ id });
  } catch (err) {
    if (err instanceof AgentiSendError) {
      // Every 4xx carries `fix` — what to do, and which endpoint does it.
      return Response.json({ code: err.code, fix: err.fix }, { status: err.status });
    }
    throw err;
  }
}
examples/next-app-router/route.ts, run by the test suite against a local AgentiSend server

The file sits at app/api/send/route.ts. A route handler runs only on the server, so the key never ships to the browser, and runtime = 'nodejs' pins it to the Node runtime. It is run by the test suite against a local AgentiSend server. The idempotency key is welcome/ plus the address: a request retried after a timeout replays the first send rather than mailing the same person twice.

Keep the id it returns

The handler answers with the message id, and that id is worth a column next to the user. Every later question has an endpoint that takes it: GET /emails/{id} for where the message is now, and GET /emails/{id}/explain for why it bounced or was held. On a refusal the handler passes code and fix through with the status the API gave, so the part of the app that called it can show the fix instead of a generic failure.

The same limits from every client

Budgets, loop detection, approvals and the kill switch behave identically whichever client holds the key, so they are not repeated here. The guardrails checklist maps each item people are told to hand-write onto the request that already enforces it, and Email for AI agents names every control with its endpoint.

Next