Agents
A daily cap on an API key: guardrails for autonomous senders
A workspace quota trips after the damage. A limit on the key trips before it. How a per-key daily cap and recipient allowlist work, why they have to be checked on every path that sends, and why the key can never loosen them itself.

Hand an agent an API key and it will use exactly what the key permits, at machine speed, until something says no. Most email APIs put that "no" on the workspace: a monthly quota, a plan limit. That is a limit on the bill. It has no opinion about the first ten thousand emails, only about the one after.
The limit an autonomous sender needs lives on the credential, and it has to say no before the first bad send, not after the ten-thousandth. SendRaven gives a key three: an approval hold, which gets its own post, and the two this one is about, a daily cap and a recipient allowlist.
The two limits
A key created with daily_send_limit: 200 can mail 200 recipients per UTC day. Recipients, not calls: a send with three addresses in to and fifty in bcc counts 53. It counts what will actually be mailed, after suppressed, opted-out and undeliverable addresses are dropped, so a list scrubbed by our own suppression checks does not eat the budget.
A key created with allowed_recipients: ["@acme.com", "ops@partner.io"] can mail those, and nobody else. An entry is an address or a domain, and a domain means that domain exactly: acme.com does not cover mail.acme.com. Anything that is neither an address nor a domain, acme or ops@, is refused when the key is saved, with the entry named. It is never quietly dropped: removing an entry from an allowlist makes it wider, and a list emptied that way allows everyone.
Both are set when the key is created, in the dashboard or over the API:
curl https://api.sendraven.ai/v1/api-keys \
-H "Authorization: Bearer $SENDRAVEN_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "support-agent",
"scopes": ["emails:send", "threads:read"],
"daily_send_limit": 200,
"allowed_recipients": ["@acme.com", "ops@partner.io"]
}'Both are checked on every send, before the message is written. The allowlist goes first:
{
"error": {
"type": "recipient_not_allowed",
"message": "This key may not send to someone@elsewhere.com. Add the address or its domain to the key's allowed recipients."
}
}That is a 403. The cap answers 429 daily_limit, with the limit and how much of it is used in the message ("This key's daily limit of 200 would be exceeded (190 already sent today)."). The order is deliberate: an agent that has wandered off gets "you may not mail this person" rather than an ambiguous quota error it might decide to wait out.
And 429 daily_limit is a stop, not a back-off. The budget comes back at midnight UTC, not in a few seconds. The Retry-After header says so in seconds, so a client that honours it waits for the right moment instead of retrying into a closed door. We wrote about why an error should tell a model what to do next separately.
A limit has to be checked on every path that sends
A cap on POST /v1/emails is a cap on one endpoint. An agent does not care which endpoint it uses, and neither does the recipient. So the question for every limit is not "is it checked?" but "is it checked everywhere mail can leave?" Here is where SendRaven asks it.
Every recipient field. cc and bcc are recipients. The allowlist checks every address in to, cc and bcc, and the cap counts all three. An allowlist that checked to alone would be a limit on who appears first in the header.
Later, as well as now. A scheduled send and a send held for approval count against the cap when they are asked for, not when they go out. Otherwise the cap limits what a key sends today and says nothing about what it queues for tomorrow morning.
Campaigns. A campaign cannot be held to an allowlist without silently becoming a different audience, and it cannot wait in an approvals queue address by address. So a key with an allowlist or an approval hold cannot send, schedule, resume or decide the A/B winner of a campaign at all, and gets 403 forbidden saying why. It can still draft and preview one, and a person can send it from the dashboard.
A key with only a daily cap can put a campaign on its way when the campaign fits what is left of its day, counted the way the campaign's preview counts it. It is charged once per campaign: starting, bringing forward, resuming or deciding a winner each count what the campaign still has to send, and a key that has already paid for that campaign pays nothing more.
Automations. A sequence sends its steps later, with no key attached. Enrolling someone is therefore a promise to mail them without asking again, so the key's limits apply at enrolment: an address outside the allowlist is 403 recipient_not_allowed, and a key with an approval hold cannot enrol anyone, because nobody would review day three.
The guardrail cannot remove itself
The agent holding the key should be able to send, read and be limited. It should not be able to rewrite its limits. Three rules, all 403 forbidden:
- A key may not edit itself.
PATCH /v1/api-keys/{id}refuses when the id is the calling key. Raising your own cap is not a foot-gun, it is the guardrail removing itself on the instruction of the thing it guards. - Nor mint a looser one. A credential can grant only scopes it holds itself. A capped key can create keys capped at or below its own number, an allowlisted key only keys allowlisted within its own list, and a held key only held keys. Otherwise the first rule is decorative: a key that cannot raise its own cap would simply make a sibling without one.
- Nor release drafts. The key that drafted a held message cannot approve it, and neither can any key with a guardrail of its own or any OAuth token. A release counts against nothing on the approving key, so an allowlisted key that could approve would be able to send anywhere by approving someone else's draft. An approval is decided by a person in the dashboard, or by a key someone deliberately left unrestricted.
Key management is also left out of our OpenAPI description and has no MCP tool, on purpose. An agent reading the spec should not be shown the door to its own limits.
Keys, not sign-ins, carry the limits
Our MCP server accepts either an API key or a sign-in through OAuth. The two are not equivalent. A sign-in grants scopes, but a daily cap and an allowlist belong to a key that someone sat down and configured, so an OAuth token carries none of them. If you want the limits in this post to apply to an agent, give it a key.
Where to start
For an agent you have not watched yet: a daily cap a little above what the job needs, an allowlist if its audience is knowable, and the approval hold for the first week. Loosen them key by key as it earns it. A cap you never hit costs nothing; a cap you did not set costs whatever the agent decided to do that afternoon.
The request and error shapes are on the guardrails page, and the docs cover limits for agents in full.



