Send from a client · Updated 2026-09-22

Send email from a Replit app

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

To send email from a Replit app, verify a domain, create a key with sending_access and a budget, store it as a project secret, and ask Replit to add a server-side send that calls the API. Replit has no AgentiSend connector. This page does not describe one, and a catalog listing for some other email vendor is not a substitute.

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 Hono. It runs on Deno unchanged, which is the runtime a Replit app can host a small server on, and it is executed against a real API on every build of this site.

The prompt to paste

Add a server-side email send to this 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. Call the API only from server code, never from 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 Hono sample at https://agentisend.com/docs/guides/hono.

The code that prompt should produce

/**
 * Hono — one POST route that sends a transactional email.
 *
 * Hono runs on Workers, Deno, Bun and Node, and the AgentiSend SDK is plain
 * fetch with no Node built-ins, so this file is the same on all of them.
 */
import { Hono } from 'hono';
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;
}

export const app = new Hono();

app.post('/send', async (c) => {
  const { email, subject, body } = await c.req.json<{
    email?: string;
    subject?: string;
    body?: string;
  }>();
  if (!email) return c.json({ error: 'email is required' }, 400);

  try {
    const { id } = await agentisend.emails.send(
      {
        from: mailFrom(),
        to: email,
        subject: subject ?? 'Your receipt',
        text: body ?? 'Thanks — your receipt is attached to your account.',
      },
      { idempotencyKey: `receipt/${email}` },
    );
    return c.json({ id });
  } catch (err) {
    if (err instanceof AgentiSendError) {
      // `fix` names the endpoint that repairs the call, so a client can act on
      // the failure without a human reading prose.
      return c.json({ code: err.code, fix: err.fix }, 502);
    }
    throw err;
  }
});

export default app;
examples/hono/app.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