SendRaven

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.

The key an agent getsPOST /v1/api-keys
{
  "name": "support-agent",
  "scopes": ["emails:send", "threads:read"],
  "daily_send_limit": 200,
  "allowed_recipients": ["@acme.com"],
  "requires_approval": true
}
When it overstepsPOST /v1/emails
403 { "error": { "type": "recipient_not_allowed" } }
429 { "error": { "type": "daily_limit" } }

202 { "status": "pending_approval",
      "approval_id": "apr_4c…" }

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.

"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": "already_decided" } }

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.

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.
  • 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_allowedThe 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_limitThe 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_approvalNot 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 message log 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.

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.