Blog

How to stop an AI agent from emailing too much or the wrong people

AI agent email guardrails on the API key: a daily send limit, a recipient allowlist and a human approval hold, with the error each one returns.

SendRaven8 min read

AI agent email guardrails belong on the API key, not in the prompt. Three per-key controls cover it. A daily send limit caps volume. A recipient allowlist caps who can receive mail. requires_approval puts a person in front of what gets sent.

All three act before the mail leaves. The agent gets a plain answer back: 429 daily_limit, 403 recipient_not_allowed, or pending_approval with an approval_id. Nothing depends on the agent behaving well.

The short answer: three limits on the key, not the agent's good behavior

A credential handed to an agent can send to anyone, as often as it likes. Workspace quotas only trip after the mail has gone. That is the gap per-key guardrails close.

ControlWhat it preventsWhat the agent sees
Daily send limitToo much mail in one UTC day429 daily_limit
Recipient allowlistMail to anyone outside approved addresses or domains403 recipient_not_allowed
requires_approvalUnreviewed content going outpending_approval with an approval_id

The limits are set on the key. The key cannot change them. That last part is the whole point, and we come back to it below.

What goes wrong when an agent holds an unrestricted key

An unrestricted key has no ceiling and no guest list. Here are three ways that hurts. They are scenarios to design against, not reported incidents.

  • The loop. A retry bug or a confused plan sends the same message over and over.
  • The fan-out. A bad tool call turns one intended recipient into a whole list.
  • The injected instruction. Text in an inbound message tells the agent to email someone else.

A workspace quota trips after the mail has gone. A per-key control sits in front of the send instead of behind it. For the longer story on volume caps, read a daily cap on an API key.

Autonomous does not have to mean unsupervised.

Per-key daily send limit and the 429 daily_limit error

Set a daily send limit on the key. Once the key reaches it, further sends return 429 daily_limit.

How the count works:

  • The day is a UTC day.
  • The count is in recipients, not messages.

Size the limit in recipients. A message that goes to several people uses up more of the cap than a message to one. The limits for agents docs are the place to confirm the details for your key.

What the agent should do on 429

Stop and report. Do not loop on retries. The agent hit a ceiling you set on purpose, and retrying will not move it. A person decides whether the limit should change.

The snippets in this article branch on the HTTP status and the error name. errorName() is a helper you write to read that name from the response. Check the API reference for the exact response body before you wire it up.

// Pseudocode. errorName() is your own helper.
if (res.status === 429 && errorName(res) === 'daily_limit') {
  return { outcome: 'stopped', reason: 'daily send limit reached' };
}

429 daily_limit is not 402 plan_limit_reached

Two limits, two status codes. A per-key cap returns 429 daily_limit. Out of plan returns 402 plan_limit_reached, not 429. A model reading the status can tell "this key has done enough for today" from "the account is out of plan". The first is for whoever manages the agent. The second is for whoever manages the bill.

The plan meter is a separate count. There, one counted email is one outbound recipient: To, Cc and Bcc each count, and retries are not counted twice.

Idempotency keys are a separate safeguard

SendRaven supports idempotency keys. A retry with the same key delivers once. Use them on sends so a flaky network or a retried tool call does not turn into a double delivery. They sit alongside the daily cap. They do not replace it.

Recipient allowlist and the 403 recipient_not_allowed error

The recipient allowlist takes addresses or domains. A send to anyone else returns 403 recipient_not_allowed.

Where it fits:

  • A support agent that may only reply to customers at known domains.
  • An internal-only agent while you are still testing.
  • A staging key limited to your own domain, so a test run cannot reach a real customer.

What it defends, and what it does not

The allowlist limits where mail can go. That narrows the damage from a bad or injected instruction. If the agent is told to email someone outside the list, the send is refused.

It does not judge message content. An allowed recipient can still receive a badly written email. For that, use an approval hold.

Treat 403 as a permanent refusal

This is not a retry case. The recipient is not allowed, and trying again will not change that. The agent should drop that recipient, tell a person, and carry on with the rest of its work.

// Pseudocode. errorName() is your own helper.
if (res.status === 403 && errorName(res) === 'recipient_not_allowed') {
  return { outcome: 'refused', reason: 'recipient not on allowlist' };
}

Human approval holds with requires_approval

Turn on requires_approval and sends from that key do not go out. They return pending_approval with an approval_id. A person releases each one from a queue.

Two details matter:

  • Held drafts have a 72-hour expiry. If nobody releases a draft in that window, it expires.
  • A key cannot approve its own drafts.

A held send is not a failure. The agent should treat pending_approval as a successful draft: store the approval_id, note that the email is waiting, and move on. Do not resend in a loop.

// Pseudocode. isPendingApproval() and approvalIdOf() are your own helpers.
if (isPendingApproval(res)) {
  await saveDraftRecord({ approvalId: approvalIdOf(res) });
  return { outcome: 'held' };
}

When to use it

  • The first weeks of a new agent, while you learn what it actually writes.
  • Emails to customers.
  • Anything with legal or financial wording.

For the reasoning, read letting an agent write email without letting it send. For the setup, follow the recipe to hold an agent's email for human approval.

A key cannot change its own limits

Here is the trust boundary. If an agent could raise its own daily cap or edit its own allowlist, the controls would be advice, not controls. A prompt-injected agent could simply give itself more room.

So the rule is plain: a key cannot change its own limits. It pairs with the approval rule. A key cannot approve its own drafts either. The agent drafts and sends within bounds. A person owns the bounds and the release.

The OAuth caveat

OAuth sessions carry scopes, but none of these limits. Scopes say what a session may do. They do not give you a daily limit, a recipient allowlist or an approval hold.

If you want those controls, give the agent an API key (sk_live_…) with the limits set. SendRaven's remote MCP server (55 tools) accepts OAuth or an API key, so pick the key when you want the limits to apply.

A sensible starting configuration

This is recommended practice, not a product default.

  1. A low daily send limit. Lower than you think you need.
  2. An allowlist of your own domain. The agent can only email your team at first.
  3. requires_approval on. Every send waits for a person.

Then loosen one control at a time. Watch what the agent actually does for a few days. Widen the allowlist when the drafts look right. Raise the limit when the volume is real. Turn approval off last, and only for mail you are happy to let go unreviewed.

One loop, four outcomes

Your agent code should handle every result. Here is the shape, in pseudocode against POST https://api.sendraven.ai/v1/emails with a sk_live_… key:

const res = await sendEmail(payload);

if (isPendingApproval(res)) {
  await saveDraftRecord({ approvalId: approvalIdOf(res) });
  return { outcome: 'held' };
}
if (res.status === 429 && errorName(res) === 'daily_limit') {
  return { outcome: 'stopped' }; // stop and report, no retry loop
}
if (res.status === 403 && errorName(res) === 'recipient_not_allowed') {
  return { outcome: 'refused' }; // permanent for this recipient
}
if (res.ok) {
  return { outcome: 'sent' };
}

Note what the two error branches do not contain: a retry. Both are stop signals. Hand them to a person.

Checklist before you let an agent send

  • The key has a daily send limit.
  • The key has a recipient allowlist.
  • requires_approval is on for anything customer-facing.
  • Agent code treats 429 daily_limit and 403 recipient_not_allowed as stop signals, not retry signals.
  • Agent code treats pending_approval as a saved draft and stores the approval_id.
  • The agent uses an API key, not an OAuth session, if you need these limits.
  • Someone owns the approval queue and watches the 72-hour expiry.

FAQ

Stopping an AI agent from sending too many emails

Set a daily send limit on the API key you give the agent. Once the key reaches the limit for the UTC day, further sends return 429 daily_limit.

What the 429 daily_limit error means, and whether the agent should retry

It means the key has reached its daily send limit. The agent should not retry. It should stop, report, and leave it to a person to decide whether to raise the limit.

What the 403 recipient_not_allowed error means

The recipient is not on the key's allowlist. Treat it as a permanent refusal for that recipient, not a transient error.

Making sure an AI agent only emails approved recipients or domains

Set the key's recipient allowlist to the addresses or domains you approve. A send to anyone else returns 403 recipient_not_allowed.

How human approval works for an AI agent's emails

With requires_approval on, a send returns pending_approval with an approval_id instead of going out. A person releases it from a queue.

What happens to a held email if nobody approves it

Held drafts have a 72-hour expiry. If nobody releases a draft in that window, it expires.

Whether the agent can raise its own limit or approve its own drafts

No. A key cannot change its own limits and cannot approve its own drafts.

How the daily send limit is counted

Per UTC day, in recipients rather than messages.

The difference between 429 daily_limit and 402 plan_limit_reached

429 daily_limit is the per-key daily cap. 402 plan_limit_reached means the account is out of plan. Out of plan returns 402, not 429.

Whether the guardrails apply over OAuth or the MCP server

OAuth sessions carry scopes but none of these limits. To get the daily limit, allowlist and approval hold, authenticate with an API key (sk_live_…) that has them set. The MCP server accepts OAuth or an API key.

Start with a limited key

Create an API key with a daily limit, an allowlist and requires_approval switched on, then connect your agent. Start free, with 3,000 emails a month. A verified payment method is required before any sending, including on Free. Read the limits for agents in the docs, or see the per-key guardrails.

  • ai agent email guardrails
  • stop ai agent sending too many emails
  • email api recipient allowlist
  • human approval for ai agent email
  • daily send limit api key

SendRaven

Try it

Give your agent an inbox, an outbox, and a seatbelt.

3,000 emails a month free, and an MCP server your agent can use today.