For agents

Email for AI agents

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.

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)"
200
{
  "api_key_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
  "budget_per_period": "string",
  "consumed_in_period": 1,
  "consumed_in_window": 1,
  "paused": true,
  "paused_at": "2026-09-04T09:14:00Z",
  "paused_reason": "string",
  "period": "hourly"
}
Response

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

{ "budget_per_period": 500, "period": "daily", "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. 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 and the fix names PATCH /limits/keys/{id}. 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.

Loop detection

The guard reads this key's sends from the last 60 minutes before every send and refuses two patterns: a candidate that would be the 4th near-identical message to the same recipient in the window, and a window of at least 20 sends that has collapsed to fewer than 3 effective recipients.

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 [email protected] within 60 minutes, which matches a retry or script loop; review the agent before sending more.",
    "fix": "Review this key's recent sends via GET /emails?api_key_id=:id to find the loop, then fix the agent before sending again.",
    "docs_url": "https://agentisend.com/docs/errors",
    "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.

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)"
201
{
  "affected_key_ids": [],
  "at": "string",
  "paused": true,
  "reason": "string"
}
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": true,
  "next_cursor": "string"
}
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
{
  "history": [],
  "next_review_at": "2026-09-04T09:14:00Z",
  "reason_codes": [],
  "reasons": [],
  "sla_deadline_at": "2026-09-04T09:14:00Z",
  "state": "ok"
}
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.

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": "Domain is not verified for sending.",
    "fix": "Add the DNS records shown by GET /domains/:id, then call POST /domains/:id/verify.",
    "docs_url": "https://agentisend.com/docs/errors",
    "retryable": false
  }
}

All 71 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.

Where everything else is