Skip to content

For agents · Published 2026-09-18 · Updated 2026-10-27 · AgentiSend

Email API for AI agents: what to require and 7 options compared

An AI agent should send email through an API that caps it, detects its loops, can be stopped by a human and explains its own refusals. This page names the endpoint for each of those.

An AI agent should use an email API that gives every key a budget it cannot raise, detects when the agent starts repeating itself, lets a human stop all sending with one request, holds risky sends for a person instead of dropping them, publishes the account's standing in JSON, and returns a fix field on every refusal. This page names the exact endpoint for each of those, with the request that calls it.

Base URL: https://api.agentisend.com. Every request carries Authorization: Bearer as_…. Every response carries ratelimit-limit, ratelimit-remaining and ratelimit-reset.

If you want the six steps from no account to a delivered email instead, read /agents. The one-sentence definition of this product is Transactional email API for AI agents.

What to require

Each requirement below is one H2. The table under it is the same requirement across seven options, verified 2026-09-28 from the linked comparison. AgentiSend's free plan is 5,000 emails a month, 170 a day, and 3 domains.

OptionPer-key budgetOne-call pauseIdempotencyInbound mail
AgentiSendOn every keyPOST /limits/kill-all7 days, every mutating callEvery verified domain, one MX record
ResendNot on the pricing pageNot on the pages fetched24 hours, two endpointsEvery plan
Amazon SESNot on the pricing pageOne call sets sending to offNot on the SendEmail pagePriced per 1,000
PostmarkNot on the pricing pageNot on the pages fetchedNot supportedPro and Platform
MailgunNot on the pricing pageNot on the pages fetchedNot on the pages fetchedRoutes, including Free
SendGridNot on the pricing pageNot on the pages fetchedNot on the mail-send pageNot on the pages fetched
AgentMailNot on the pricing pageNot on the pages fetched24 hours on sendInboxes are the product

Loops and Plunk are compared on their own pages: /vs/loops, /vs/plunk.

Per-key budgets

A budget belongs to an API key, not to the account, and it is checked before the send rather than reported after it. Give each agent its own key so one agent's ceiling is not another's.

curl -sS -X PATCH https://api.agentisend.com/limits/keys/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42 \
  -H "Authorization: Bearer $AGENTISEND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"budget_per_period":500,"rate_ceiling_per_minute":20}'
200
{
  "api_key_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
  "budget_per_period": -9007199254740991,
  "consumed_in_period": -9007199254740991,
  "consumed_in_window": -9007199254740991,
  "paused": false,
  "paused_at": "2026-09-04T09:14:00.000Z",
  "paused_reason": "example",
  "period": "hourly",
  "period_started_at": "2026-09-04T09:14:00.000Z",
  "rate_ceiling_per_minute": -9007199254740991,
  "rate_window_started_at": "2026-09-04T09:14:00.000Z",
  "updated_at": "2026-09-04T09:14:00.000Z"
}
Response

Every field in the body is optional, and you send the ones you want to change:

{ "budget_per_period": 500, "rate_ceiling_per_minute": 20 }

period is hourly, daily or monthly. budget_per_period is a count of sends; rate_ceiling_per_minute is a separate per-minute ceiling. An API key can only lower its limits, and a shorter period is compared over 30 days: 500 a day is up to 15,000, far more than a new Free key's 5,000 a month, so a change like that is made by a person in the console. Read the current state of one key with GET /limits/keys/{id}, or the whole fleet with GET /limits/keys.

When the budget is spent the send is refused with agent_budget_exceeded. Nothing is queued behind the refusal, nothing is charged, and no request the agent can make raises its own ceiling. When the account's plan inclusion is spent instead, the code is plan_limit_reached.

Longer version: /agent-email-budgets. The category page that lines the usual DIY checklist up against these calls is Email API with spending limits for AI agents.

Loop detection

The guard reads this key's sends from the last 60 minutes before every send. It refuses a candidate that would be the 4th near-identical message to the same recipient. On a key set to full, and on a marketing send from a key with no setting, it also refuses the next send to an inbox that a window of at least 20 sends has collapsed onto, once that window has fewer than 3 effective recipients. Mail to anyone else still sends. A new key is repetition-only. whoami reports loop_guard.

A refused send answers approval_required with the evidence attached, and the held message is recorded in the approval queue rather than discarded.

{
  "error": {
    "code": "approval_required",
    "message": "Blocked: this agent has sent 3 near-identical emails to customer@example.com within 60 minutes, which matches a retry or script loop; review the agent before sending more.",
    "fix": "Do not send it again: a person approves or rejects it in the console under Agents → Approvals, and approving sends it — the message then appears in GET /emails. action_id in this error names the held send; the key that asked cannot approve itself, and a retry waits on the same approval.",
    "docs_url": "https://agentisend.com/what-does-approval-required-mean#approval_required",
    "retryable": false
  }
}

GET /limits/keys returns loop_state per key, one of clear, watch or tripped, with the evidence behind it.

Longer version: /agent-email-loop-detection. When a loop is already running, pause the key and read the holds on Stop an AI agent sending emails in a loop.

The kill switch

One request pauses every key on the account:

curl -sS -X POST https://api.agentisend.com/limits/kill-all \
  -H "Authorization: Bearer $AGENTISEND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
200
{
  "affected_key_ids": [],
  "at": "2026-09-04T09:14:00.000Z",
  "paused": false,
  "reason": "example"
}
Response

The reply lists affected_key_ids and the time it took effect. Sends from a paused key are refused with kill_switch_active. Nothing auto-resumes: POST /limits/resume-all brings the account back, POST /limits/keys/{id}/resume brings one key back, and budgets and ceilings are unchanged by either. One key can be stopped on its own with POST /limits/keys/{id}/kill.

If you are the agent and your sends start answering kill_switch_active, a person did that on purpose. Report it; do not route around it.

Approval queues

A held send is a row, not a loss. List what is waiting:

curl -sS -X GET https://api.agentisend.com/agent-actions \
  -H "Authorization: Bearer $AGENTISEND_API_KEY"
200
{
  "data": [],
  "has_more": false,
  "next_cursor": "example",
  "object": "list"
}
Response

Each row carries state (pending, approved, rejected or killed) and a preview with the subject, to_count, the first three recipients, the key that asked and held_reason. A human approves with POST /agent-actions/{id}/approve, which executes the held send through the same path a normal send takes and returns the message_id, or rejects with POST /agent-actions/{id}/reject and a reason that is kept with the row.

An agent that reads approval_required as a failure and retries fills the queue. Poll the action instead.

Longer version: /agent-email-approvals.

Machine-readable standing

Check standing before a large send instead of discovering it mid-run:

curl -sS -X GET https://api.agentisend.com/trust/standing \
  -H "Authorization: Bearer $AGENTISEND_API_KEY"
200
{
  "domains": [],
  "history": [],
  "next_review_at": "2026-09-04T09:14:00.000Z",
  "probation": false,
  "probation_cap": -9007199254740991,
  "probation_ends_at": "2026-09-04T09:14:00.000Z",
  "reason_codes": [],
  "reasons": [],
  "sla_deadline_at": "2026-09-04T09:14:00.000Z",
  "state": "ok",
  "streams": {
    "marketing": {
      "bounce_rate": 1,
      "bounced": -9007199254740991,
      "complained": -9007199254740991,
      "complaint_rate": 1,
      "sent": -9007199254740991,
      "state": "ok"
    },
    "transactional": {
      "bounce_rate": 1,
      "bounced": -9007199254740991,
      "complained": -9007199254740991,
      "complaint_rate": 1,
      "sent": -9007199254740991,
      "state": "ok"
    }
  },
  "window_days": -9007199254740991
}
Response

state is ok, warning, throttled or paused. reasons carries the metric (bounce or complaint), the measured value, the threshold it is judged against, the window, and sample messages. The thresholds themselves are published at GET /trust/thresholds: bounce rate warns at 2% and pauses at 4%, complaint rate warns at 0.05% and pauses at 0.08%, and no step is taken until 100 delivered-or-bounced sends in the window. Complaint counts come from mailbox providers that send us feedback reports; Gmail, Yahoo and Outlook do not send them to us today, so a complaint rate of 0 is not evidence that nobody complained.

If state is not ok, fixing the list is the move, not sending more.

The fix field

Every 4xx and 5xx carries code, message, fix, docs_url and retryable. fix is an instruction that names the endpoint to call next, so an agent has something to do other than retry. retry_after_seconds is present exactly when retryable is true.

{
  "error": {
    "code": "domain_not_verified",
    "message": "This sending domain is not verified.",
    "fix": "If it is pending, publish the required records from GET /domains/:id; checks continue for 72 hours. Test now by sending to an address ending in @simulator.agentisend.com. If it is not registered, POST /domains first. If it is failed, GET /domains/:id names the record to fix, then POST /domains/:id/verify to reopen the window.",
    "docs_url": "https://agentisend.com/docs/errors#domain_not_verified",
    "retryable": false
  }
}

All 122 codes are at /docs/errors, and as Markdown at /docs/errors.md.

To find out whether a send would pass every gate without sending it, call POST /emails/preflight. It runs the domain, sandbox, trust, suppression, budget, loop and content checks through the same code path POST /emails uses.

MCP

The same API, same limits, same error contract, over one streamable HTTP endpoint with bearer auth:

https://api.agentisend.com/mcp

The tools that matter to the control plane are get_agent_budget, set_limit, get_kill_switch_state, list_pending_approvals, request_approval, get_trust_standing, preflight_email and why_was_this_not_sent. Every tool that changes something accepts idempotency_key. The full catalogue with the scope each tool needs is at /docs/guides/mcp; client configuration is at /docs/guides/agents.

There is no looser path in over MCP. An agent gets the same budget, the same loop guard and the same kill switch it gets over REST.

Idempotency

Send Idempotency-Key on every call that changes something. Repeating the call with the same key replays the first result instead of doing the work twice, which is what makes a crashed agent safe to restart. Keys are remembered for 7 days.

Two codes exist so a retry is never ambiguous: idempotency_in_flight when the first request is still running, and idempotency_payload_mismatch when the same key arrives with a different body.

What this service will not do

No cold outreach. There is no lead database, no scraping, and no bulk import of addresses that never asked to hear from you. A request that looks like unsolicited sending is refused, and asking differently will not change the answer.

Send from your client

One page per client. The title is the question. Editors and coding agents that speak MCP get the config on the page. App builders get a prompt and a server-side sample, because none of them has a connector.

Whether you have to build those guardrails yourself: /agent-email-guardrails.

Questions

The title is the question. The first sentence is the answer.

Jobs

Seven jobs, each with the ceiling for that key, a snippet the test suite runs, the refusal that job hits, and one limit:

The index is Use cases.

Where everything else is