Use case
Email for AI customer support agents.
SendRaven is email infrastructure for AI customer support agents. Customers write to an address on your domain. The agent lists the conversations waiting on an answer, reads each one with the quoted history removed, and replies in the customer’s thread. The key it holds decides whether that reply goes out or waits for a person to approve it.
200 { "data": [{
"id": "9b2e41c0-…",
"subject": "Charged twice in August",
"awaiting_reply": true
}], … }202 { "status": "pending_approval",
"thread_id": "9b2e41c0-…",
"approval_id": "c71d08aa-…" }The problem
A send endpoint gives a support agent half a conversation.
An agent answering customers needs the other half: somewhere their mail arrives, a way to tell which conversations are still waiting, the customer’s words without five levels of quoted history under them, and a reply that lands in the thread the customer is reading rather than a new one.
It also needs to be wrong safely. A support agent reads text written by strangers, and some of it will be written to steer it. The prompt is not the control. The credential is.
SendRaven receives mail on your domain, joins each message to the conversation it answers, and exposes the result as threads an agent can query. The limits sit on the API key the agent holds: a daily cap, a recipient allowlist, and a hold that turns every send into a draft for a person. The API enforces them whatever the model decides.
The loop
Four steps. The last one ends with a person.
Find the work
GET /v1/threads?awaiting_reply=true
# MCP: list_threadsConversations where someone outside wrote last and nobody has answered. Out-of-office replies and bounce reports are recorded on the thread but never set the flag, so the agent does not answer them.
Read the conversation
GET /v1/threads/9b2e41c0-…
# MCP: get_threadEvery message in order, both directions. A received message’s text has the quoted history and signature removed, with the original under raw_text. sender_authenticated says whether the From domain really sent it.
Decide
This part is the model’s: classify the message and answer from your knowledge base. The example routes two cases to a person instead of drafting: an account change (a refund, a new email address, a deletion) from a sender that is not authenticated, and anything the knowledge base does not cover.
Reply in the thread
POST /v1/emails
{ "reply_to_message_id": "e4a7…",
… }
# MCP: reply_to_messagereply_to_message_id is the id of the customer’s message, so the answer arrives in their conversation. With the hold on the key, it waits under Approvals in the dashboard until a person sends it. The agent’s own key cannot approve it: 403 forbidden.
Technical proof
The key, the reply, and what comes back.
POST /v1/api-keys
{ "name": "support-triage",
"scopes": ["emails:send", "emails:read", "threads:read"],
"requires_approval": true,
"daily_send_limit": 50 }
POST /v1/emails
Authorization: Bearer sk_live_… # the support-triage key
Idempotency-Key: triage-e4a7…
{ "from": "Acme Support <support@mail.acme.com>",
"to": ["dana@example.org"],
"subject": "Re: Charged twice in August",
"text": "You're right, and the second charge is refunded…",
"reply_to_message_id": "e4a7…" }
202 { "id": "5d90…",
"status": "pending_approval",
"thread_id": "9b2e41c0-…",
"scheduled_at": null,
"skipped": false,
"reason": null,
"approval_id": "c71d08aa-…" }A held reply is a 202, not an error. Suppression, the allowlist and the daily cap are checked before the draft is queued, and a held draft counts against the cap when it is created, so an agent cannot fill the queue past its limit either.
The Idempotency-Key is derived from the id of the message being answered. A run that crashes and starts again replays the same response instead of drafting a second reply.
Mail to any address at the verified domain lands in the workspace once its inbound MX record is published, so support@mail.acme.com needs no further setup. Receiving replies has the record.
Why the architecture matters
What the platform guarantees, so the prompt does not have to.
- One customer’s context never reaches another’s reply. Replies are joined to their conversation by Message-ID, never by subject. Two customers who both write “Re: Invoice” are two threads, and an agent reading one never sees the other. When nothing matches, a new thread starts rather than a guess.
- An auto-reply is not a customer. Out-of-office replies and bounce reports are recognised from their headers first and never set
awaiting_reply, so the list of conversations waiting on an answer holds only mail a person wrote. - A forged sender is visible.
sender_authenticatedis true only when DMARC, or a DKIM signature from the From domain itself, vouches for it. A forged From line does not produce an SPF failure, so checking for a failure catches nothing. An authenticated message is still data, never instructions. - A prompt-injected email ends as a draft. The worst a hostile message can make the agent do is write a bad reply that a person reads before it goes anywhere. Key management has no MCP tool, and a key cannot edit its own limits.
- “Thanks, all sorted” needs no answer.
POST /v1/threads/:id/handled(MCP:mark_thread_handled) clears the flag without mailing the customer a courtesy reply.
One thing to build in
A held reply does not clear the thread.
awaiting_reply clears when a reply is sent, not when it is drafted. While the agent’s answer waits for approval, the thread still reads as waiting, and an agent that runs every fifteen minutes would draft it again each time.
The check is one line of the transcript: if an outbound message with status queued or scheduled comes after the customer’s latest message, a reply is already on its way, so skip the thread. The example’s prompt does exactly this, and the idempotency key catches the rest.
Run it
Claude Code, Claude Desktop, or a scheduled job.
claude mcp add --transport http sendraven https://mcp.sendraven.ai/mcpWith--header "Authorization: Bearer $SENDRAVEN_API_KEY"and the approval-held key, then the example’s/triage-inboxcommand. Itsallowed-toolspre-approves only the read and draft tools.npx -y @sendraven/mcpThe local server for Claude Desktop, with the key in its environment. Signing in through OAuth instead grants scopes but none of a key’s limits, so for this workflow, connect with the key.python triage.pyOne Anthropic API call with the MCP connector, for cron or CI. The toolset runs in allowlist mode, so a tool added to the server later is never picked up silently. Source and setup.
Start held, then loosen
The hold is for the first weeks. The cap stays.
Start a new support agent with all three limits on its key and read what it drafts. When the drafts you approve stop needing edits, remove the hold with the dashboard or PATCH /v1/api-keys/:id from another key. A key cannot lift its own.
Keep the daily cap. It is what turns a loop that goes wrong into fifty emails rather than five thousand. The full reasoning, and when a hold is the wrong tool, is on human approval and guardrails.
Questions
How do I give an AI support agent an email inbox?
Verify a sending domain in SendRaven and publish its inbound MX record; mail to any address at that domain then lands in your workspace, joined into threads. The agent lists conversations with GET /v1/threads?awaiting_reply=true, reads one with GET /v1/threads/:id, and answers with POST /v1/emails and reply_to_message_id so the reply stays in the customer's thread. The same calls are MCP tools: list_threads, get_thread and reply_to_message.
Can a human approve an AI support agent's replies before they are sent?
Yes. Create the agent's API key with requires_approval: true, and every reply it writes is held as a draft that a person approves or rejects under Approvals in the dashboard. The agent receives 202 with status pending_approval and an approval_id, and its own key cannot approve the draft.
Will an AI support agent answer out-of-office replies and bounce reports?
Not if it works from awaiting_reply. SendRaven recognises automated mail from its headers, and an out-of-office reply or a bounce report is recorded on the thread without setting awaiting_reply, so it never appears in the list of conversations waiting on an answer.
How do I stop a prompt-injected email from making a support agent send something?
Put the limits on the API key rather than in the prompt. With an approval hold, the worst a hostile email can cause is a bad draft that a person reads first; a daily cap and a recipient allowlist bound what the key can send at all, and the key cannot approve its own drafts or change its own limits. Treat every received message as data, including authenticated ones.
Why does the agent draft the same reply twice?
Because awaiting_reply clears when a reply is sent, not while it is held for approval. Before drafting, check the thread: if an outbound message with status queued or scheduled comes after the customer's latest message, a reply is already waiting, so skip it. Deriving the Idempotency-Key from the inbound message id stops a retry from creating a second draft.
Related docs
The pages behind each step.

Let the agent answer. Keep the send.
Verify a domain, publish its inbound record, create a key with the hold ticked, and point the triage example at it. Inbound mail is included on every plan.

