Send from a client · Updated 2026-09-22

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. This page does not describe one. Bolt produces a Next.js app, so the sample to ask for is a route handler, not an edge function and not a client component.

What to set up before the prompt

Verify a sending domain, then create a key with POST /api-keys and permission sending_access. Set its ceiling with PATCH /limits/keys/{id}: budget_per_period, period (hourly, daily or monthly) and rate_ceiling_per_minute. Store the key as a project secret. The sample reads AGENTISEND_API_KEY and MAIL_FROM. The From address has to be on the domain you verified. The file below is executed against a real API on every build of this site.

The prompt to paste

Add a server-side email send to this Next.js app. There is no native AgentiSend connector to enable. Read the API contract at https://agentisend.com/openapi.json and the docs at https://agentisend.com/docs. The sender must be an address on a domain that has been verified. Create an API key with permission sending_access and a budget, and store it as a project secret named AGENTISEND_API_KEY. Store the From address as MAIL_FROM. Put the send in an App Router route handler so the key never reaches the browser, and send an Idempotency-Key on the send so a retry does not mail twice. Use the package agentisend. If the API refuses the send, show the code, the message and the fix, and do not retry when retryable is false. The handler should match the route sample at https://agentisend.com/docs/guides/nextjs.

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, executed against the live API on every build

What the agent can and cannot do

The key's permission is sending_access. It can call POST /emails and read GET /emails/{id} for a message that key sent. It cannot raise budget_per_period, and there is no request the app's key can make that lifts its own ceiling. A send that looks like a loop is held for a person. List the queue with GET /agent-actions. A person approves with POST /agent-actions/{id}/approve or rejects with POST /agent-actions/{id}/reject.

A person stops that one key with POST /limits/keys/{id}/kill, or every key on the account with POST /limits/kill-all. Nothing starts again until a person resumes it. The longer versions are Budgets and the kill switch and Approvals.

What a refusal looks like

A send past the budget answers with code, message, fix and docs_url. The fix names the call.

{
  "error": {
    "code": "agent_budget_exceeded",
    "message": "Key budget for the current period is exhausted.",
    "fix": "Wait for the period to reset. get_agent_budget and GET /limits/keys/:id both say when. Raising a budget is a person's decision, made in the console; a key cannot raise its own.",
    "docs_url": "https://agentisend.com/docs/errors",
    "retryable": false
  }
}

retryable is false, so the app stops instead of spinning. If the key has been paused, the code is kill_switch_active and the fix names GET /trust/standing.

Next