Limits on the credential
An agent holds a key, not a workspace. So the limits live on the key.
A daily cap, a recipient allowlist and a human approval hold, set when the key is created and checked on every send before a message row is written.
A workspace quota trips after an agent has mailed thousands of people. These trip before.
{
"name": "support-agent",
"scopes": ["emails:send", "threads:read"],
"daily_send_limit": 200,
"allowed_recipients": ["@acme.com"],
"requires_approval": true
}403 { "error": { "type": "recipient_not_allowed" } }
429 { "error": { "type": "daily_limit" } }
202 { "status": "pending_approval",
"approval_id": "apr_4c…" }Built for autonomous senders
Give agents email. Keep the keys.
A backend service and an autonomous agent should not hold the same kind of credential. SendRaven lets every API key carry its own limits, checked before an email leaves the building.

- daily_send_limit
Daily send limit
200 emails a day means email 201 does not happen.
Put a hard ceiling on what one agent can send. Scheduled and held sends count toward it.
200email 201 does not happen
- allowed_recipients
Recipient allowlist
Internal only? Customer domains only? Say so on the key.
Restrict a credential to specific addresses or whole domains, checked per recipient, cc and bcc included.
- maya@acme.com (allowed)
- @acme.com (allowed)
- 403 recipient_not_allowed (refused)
- requires_approval
Human approval
Let the agent draft. Let a human decide.
A key can create email without permission to release it. Held sends wait in an approval queue instead of going out, and the key cannot approve its own.
- status
- pending_approval
- undecided draft expires
- 72h
Autonomous does not have to mean unsupervised.
Three controls
Set once, on the key. Checked on every send.
daily_send_limit
A hard ceiling per key per UTC day, counted in recipients. A send that would cross it is refused whole, not half-delivered.
This is a stop, not a back-off: an agent looping on it burns its budget against a closed door.
"daily_send_limit": 200
429 { "error": {
"type": "daily_limit",
"message": "This key's daily limit of 200
would be exceeded (200 already sent
today)." } }allowed_recipients
Full addresses or whole domains. @acme.com matches the domain exactly: acme.com.evil.com does not match, and neither does a lookalike.
An empty list means no restriction. Checked before the daily cap, so a misdirected send gets a clear reason instead of an ambiguous quota error.
"allowed_recipients":
["ops@acme.com", "@acme.com"]
403 { "error": {
"type": "recipient_not_allowed",
"message": "This key may not send to
x@other.com. Add the address or its
domain to the key's allowed
recipients." } }requires_approval
The key drafts instead of sending. The message is persisted, stamped with its Message-ID, and visible in the log, but nothing goes out until a person releases it.
Without this the choice is to trust the agent completely or not give it email at all. How the hold works.
"requires_approval": true
202 { "id": "msg_8a1…",
"status": "pending_approval",
"approval_id": "apr_4c…" }The approvals queue
The agent drafts. A person releases.
What is waiting
GET /v1/approvals
{ "object": "list", "data": [ {
"id": "apr_4c…",
"message_id": "msg_8a1…",
"api_key_id": "key_2f…",
"expires_at": "2026-09-03T09:14:00Z",
"message": {
"to": ["maya@acme.com"],
"subject": "Re: Your invoice",
"text": "Attached as a PDF, as asked.",
"html": "…"
}
} ] }The queue carries the message content, so the reviewer reads what would be sent rather than a reference to it. It is a list of things waiting on a human, so it is short by definition.
The decision
POST /v1/approvals/:id
{ "decision": "approve",
"decided_by": "daniel@acme.com" }
200 { "id": "apr_4c…",
"status": "approved",
"message_id": "msg_8a1…" }
# or
{ "decision": "reject",
"reason": "wrong invoice",
"decided_by": "daniel@acme.com" }
# a second reviewer, a moment later
409 { "error": { "type": "invalid_state" } }Approving dispatches the held message; rejecting cancels it. The decision is conditional on the approval still being pending, so two reviewers clicking at once cannot send the same message twice. Approvals expire if nobody decides.
The reviewer is a person in the dashboard or a key without guardrails; the drafting key, a guarded key and any OAuth token get 403 forbidden.
Why the key
A workspace quota is the wrong place.
The motivating case is an autonomous agent holding a credential. Every failure below is one a workspace-level limit sees only after it has happened.
The quota trips after the damage.
A monthly allowance at the workspace level has no opinion about the first ten thousand sends, only the ten-thousand-and-first. A per-key daily cap has an opinion about the two-hundred-and-first.
One key’s mistake stops everyone’s mail.
A workspace limit hit by a looping agent also blocks the deploy pipeline’s password resets. A key limit stops the key.
A prompt-injected agent mails whoever it is told to.
An allowlist on the key means the answer to “email this list of addresses” is 403 for every address outside the domain the key was issued for, before anything is written.
A campaign is not a way around the key.
A key with an approval hold or an allowlist gets 403 forbidden for sending, scheduling or resuming a campaign, because an audience cannot be held per address or narrowed to an allowlist without becoming a different audience.
A capped key starts one only when its recipients fit what is left of the day, and they count against the cap.
The check runs before persistence. Guardrails are evaluated per send, before the message row exists. There is no window in which a held or refused send is half-done.
The exact shapes
Two errors, and one status that is not an error.
403 recipient_not_allowed
The key’s allowlist excludes this address. Do not try another spelling. Ask a person to add the address or its domain to the key.
429 daily_limit
The key’s daily budget is spent. Retrying does not help until the UTC day rolls over.
Distinct from 429 rate_limited, which is too fast and is worth a pause; and from 402 plan_limit_reached, which is the workspace out of plan.
202 pending_approval
Not an error. The message is drafted and waits at GET /v1/approvals. There is nothing to retry; an agent that treats it as a failure and sends again queues a second draft.
llms.txt and the MCP tool descriptions say this in as many words. All errors.
MCP clients
Scopes, granted on a consent screen.
emails:send Send email on your behalf
emails:read Read your sent emails and delivery status
threads:read Read inbound replies and conversations
contacts:write Add and update contacts, and record opt-outs
broadcasts:write Create and send campaigns to your audiences
domains:write Add, verify and remove sending domains
…A key carries only the scopes it is granted. A support agent that reads threads and replies has no business sending campaigns, and scoping the key that way limits what a leak or a bad prompt can do.
Creating a key is itself gated on domains:write, so a key that can only send cannot mint one that can do more.
The remote MCP server signs in with OAuth and holds no credentials of its own. The consent screen lists the scopes being granted in those words, so a person authorising an MCP client sees “Send email on your behalf” rather than a string, and the client gets exactly that and nothing more. Authentication.
Questions
How do you stop an AI agent from sending too many emails?
Put a daily cap on the API key the agent holds, not on the workspace. checkGuardrails() runs before anything is persisted, and the cap counts scheduled and held messages as well as dispatched ones — a workspace quota only trips once the damage is done.
Can you require human approval before an AI agent sends an email?
Yes — set requires_approval on the key and every send becomes a draft held for a person. The send returns status pending_approval, which is not an error and has nothing to retry; releasing it applies the schedule the message was created with.
How do you limit which addresses an AI agent can email?
An allowlist on the key, checked per recipient before the message is written. It is the same check whether the agent sends to one address or fifty, because a guardrail that only runs on single-recipient sends is not a guardrail.

Give the agent a key it cannot hurt you with.
Create the key under Developers, set a cap, an allowlist and a hold, and the MCP tools respect all three. Then see what the platform refuses on its own: deliverability.

