How to Send Email From an AI Agent (MCP, SDK and REST)
Three ways to let an AI agent send email, step by step, with the key setup, test addresses and refusal handling that keep one agent from damaging a sending domain.
Direct answer. To send email from an AI agent, give the agent its own AgentiSend API key with a send budget, then connect it one of three ways: the hosted MCP server at https://api.agentisend.com/mcp for clients such as Claude and Cursor, a tool built on the agentisend SDK for agent frameworks, or plain POST /emails over REST for anything else. All three run under the same budget, loop guard and kill switch. If your agent needs a full inbox of its own rather than a way to send from your domain, an inbox product such as AgentMail is the better starting point.
How we put this together
Our editors read each provider's public pricing page on 9 November 2026 and its documentation between 28 September and 10 November 2026, and took every code sample's shape from AgentiSend's published docs and examples. We did not run these samples against a live account of our own.
Pick the path that matches your agent
| Path | Use it when | How the agent authenticates | Retry protection | What a refusal looks like |
|---|---|---|---|---|
| MCP | The agent lives in Claude, Cursor, Claude Code, Codex or another MCP client | Bearer key in a header, or OAuth 2.1 | idempotency_key argument on every writing tool |
Tool result with code, message, fix, retryable |
| SDK tool | You build the agent with a framework (Vercel AI SDK, LangChain, OpenAI Agents SDK, Mastra, CrewAI) | AGENTISEND_API_KEY read by new AgentiSend() |
{ idempotencyKey } option on emails.send |
AgentiSendError with code and fix |
| REST | Any language, a workflow tool, or a shell script | Authorization: Bearer as_… |
Idempotency-Key header, kept 7 days |
JSON error object with the same fields |
The paths share one account and one set of limits. An agent cannot escape its budget by switching from MCP to REST.
Step 1: authenticate the domain the agent will send from
Mailbox providers decide on the domain, and an agent's mail is judged with everything else that domain sends. Use a subdomain for agent mail if you can, so its results stay apart from your sign-in codes.
Add the domain with POST /domains, then publish the records GET /domains/{id} returns:
| Record | Host (for agent.example.com in the example.com zone) |
Needed |
|---|---|---|
| DKIM TXT (2048-bit key) | as1._domainkey.agent |
Required |
| Return-path MX | send.agent |
Required |
| Return-path TXT | send.agent |
Required |
SPF TXT, include:_spf.agentisend-dns.com |
agent |
Recommended |
| DMARC TXT | _dmarc.agent |
Recommended |
The domain verifies when the three required records resolve. The return-path pair is what makes SPF pass in alignment with your From domain, which DMARC checks.
An agent can also do this part itself: setup_domain returns the zone file, domains_setup_link returns a page the person who owns DNS can open without signing in, and wait_for_domain blocks until the records resolve.
Step 2: give the agent its own key, with a ceiling
Never hand an agent the key your backend uses. A shared key means a shared budget and an audit log that cannot tell the two apart. Create a key scoped to sending, then cap it:
import { AgentiSend } from 'agentisend';
const owner = new AgentiSend(); // your full-access key, kept by you
const key = await owner.apiKeys.create({
name: 'support-triage-agent',
permission: 'sending_access',
});
await owner.limits.update(key.id, {
budget_per_period: 200,
rate_ceiling_per_minute: 5,
});
// key.token is what the agent gets
sending_access cannot read your logs, change domains or mint wider keys. The agent can lower its own ceilings with set_limit but a request to raise them is refused with human_action_required. If you skip the budget, the key is still capped at the plan's monthly inclusion.
An agent with no human at the keyboard can open an account itself: agentisend agent sign-up --name <name> --human <email>, then agentisend agent verify --code <code> once the human has the code. Until then the key can mail only that human.
Step 3a: send over MCP
For Claude Code:
claude mcp add --transport http agentisend https://api.agentisend.com/mcp \
--header "Authorization: Bearer as_YOUR_AGENT_KEY"
For Cursor, the same URL and header go in .cursor/mcp.json under mcpServers. In claude.ai, AgentiSend is in the connector directory at claude.ai/directory/agentisend and connects over OAuth. Clients that only speak stdio can run npx -y @agentisend/mcp-server with AGENTISEND_API_KEY set.
Then ask the agent to call whoami first: it reports what the connection may do and how much budget is left. send_email sends; preflight_email runs every check without sending.
Step 3b: send from an SDK tool
In a framework, wrap the send as a tool that returns refusals instead of throwing, so the model can read them. With the Vercel AI SDK:
import { tool } from 'ai';
import { z } from 'zod';
import { AgentiSend, AgentiSendError } from 'agentisend';
const agentisend = new AgentiSend(); // the agent's own key
export const sendEmail = tool({
description:
'Send one email to a person who asked for it. If the fix says a person decides, stop and report it.',
inputSchema: z.object({
to: z.string(),
subject: z.string().min(1).max(200),
text: z.string().min(1),
purpose: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/),
}),
execute: async ({ to, subject, text, purpose }) => {
try {
const { id } = await agentisend.emails.send(
{ from: process.env.MAIL_FROM!, to, subject, text },
{ idempotencyKey: `${purpose}/${to}` },
);
return { sent: true, id };
} catch (err) {
if (err instanceof AgentiSendError) return { sent: false, code: err.code, fix: err.fix };
throw err;
}
},
});
The purpose argument is the detail that protects deliverability. The idempotency key is built from what the message is for and who it goes to, so a model that calls the tool twice for the same ticket update sends once. A key built from the time would send twice.
Step 3c: send over REST
curl -X POST https://api.agentisend.com/emails \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-4182-update/[email protected]" \
-d '{"from":"Support <[email protected]>","to":"[email protected]","subject":"Ticket 4182: we are on it","text":"Someone from support will reply within the hour."}'
A success returns {"id": "…"}. Up to 500 messages can go in one POST /emails/batch, each accepted or refused on its own.
Step 4: rehearse with addresses that reach nobody
Before an agent touches a real inbox, point it at simulator.agentisend.com. delivered@ delivers, bounced@ bounces, complained@ produces a complaint, suppressed@ behaves like a suppressed recipient, and delayed@ reports a delay before delivering. These sends cost nothing, never reach a mail server, and never count toward your domain's bounce or complaint rate. A label after + (for example [email protected]) lets you tell test runs apart. One real address in the recipient list makes the whole send real, so keep test runs pure.
Step 5: teach the agent what each refusal means
| Code | What happened | What the agent should do |
|---|---|---|
agent_budget_exceeded |
The key's budget is spent | Stop and report; a person raises it, or the period resets |
plan_limit_reached |
The account's plan inclusion is spent | Stop and report |
approval_required |
The loop guard held the send (4th near-identical send to one person in 60 minutes) | Stop; a person approves or rejects. Retrying returns the same action_id |
kill_switch_active |
A person or key paused sending | Stop; only a signed-in person resumes |
human_action_required |
The agent tried something only a person may do | Stop and say so |
domain_not_verified |
The From domain has not verified | Read fix; it names the call to make |
Every refusal carries retryable. When it is false, waiting will not help and the agent must not loop. When it is true, retry_after_seconds says exactly how long to wait.
Step 6: watch the numbers that protect the domain
Subscribe a webhook to limit.warning (80% of a budget), limit.exceeded, email.bounced, email.complained and agent.approval_requested. AgentiSend warns at 2% bounces or 0.05% complaints and pauses at 4% or 0.08% over a rolling 7 days, never on fewer than 100 sends. Complaint figures come only from providers that send feedback reports, which Gmail, Yahoo and Outlook do not do today, so a zero is not a clean bill of health. Our guide on handing an agent an email key covers the human side of that review.
Other ways to give an agent email
AgentiSend
Best for: agents that send from your own domain through MCP, an SDK or REST under one set of limits. Its overview of the agent controls is at agentisend.com, and the full MCP tool list is in the AgentiSend MCP guide.
AgentMail
Best for: agents that need their own inboxes, with a hosted MCP server of 39 tools.
Watch out for: limits are per plan (1,000 a day on the $20 Developer plan), not per key.
Resend
Best for: teams on Resend who want its remote MCP server with OAuth sign-in.
Watch out for: no per-key send budget is listed, and idempotency keys last 24 hours.
Plunk
Best for: a local MCP server that asks before sending to several recipients.
Watch out for: caps apply per project and category, so agents in one project share them.
Questions people ask
Can an AI agent send email without a human approving each one? Yes, within its budget. A person is only needed when the loop guard holds a send, when the agent asks with request_approval, or when a ceiling needs raising.
Which is better for an agent, MCP or an SDK? MCP when the agent runs inside a client you do not control, such as Claude or Cursor. An SDK tool when you write the agent loop yourself and want to shape what the model sees.
How do I stop an agent from sending duplicates? Derive the idempotency key from the purpose and the recipient, never from the time. AgentiSend remembers each key for 7 days.
Will an agent's email go to spam? Not because an agent sent it. It goes to spam for the usual reasons: missing authentication, bounces from guessed addresses, and complaints from people who did not want it.
Sources
Documentation read 28 September to 10 November 2026: docs.agentmail.to/integrations/mcp, agentmail.to/pricing, resend.com MCP documentation, resend.com/docs/dashboard/emails/idempotency-keys, docs.useplunk.com/guides/mcp-server, docs.useplunk.com/concepts/billing. Pricing pages read 9 November 2026: agentmail.to/pricing, resend.com/pricing.