CarlyEmail docs

Make your agent reply to email

Give the agent an inbox, give it email tools, and wake it when mail arrives.

Four steps, and the agent you already have stays as it is. By the end, someone emails your agent and gets an answer in the same thread.

person sends email
        ↓
CarlyEmail stores and authenticates it
        ↓  signed message.received event
your runtime resumes the agent for this thread
        ↓  MCP or SDK tools
agent reads context, drafts, sends, or replies
        ↓
CarlyEmail sends a correctly threaded email

1. Give the agent an inbox

npx carlyemail signup --human-email you@example.com --username assistant
npx carlyemail verify 123456

The CLI stores the API key in ~/.carlyemail/config.json. Put that key and the inbox it printed in your application's secret store:

export CARLYEMAIL_API_KEY=ce_us_...
export CARLYEMAIL_INBOX=assistant@carlyemail.com

Do not ship the key to a browser. For a deployed agent, issue an inbox-scoped key with only the permissions it needs.

2. Give it email tools

Use MCP when the framework has an MCP client. It discovers the live tool schemas, so you do not maintain wrappers:

URL: https://api.carlyemail.com/mcp
Authorization: Bearer $CARLYEMAIL_API_KEY

LangChain, the OpenAI Agents SDK, the Claude Agent SDK, Vercel AI SDK, Mastra, and Eve all support this directly. Use the TypeScript or Python SDK when you want to choose a smaller surface or call CarlyEmail from ordinary application code.

The model does not need every operation. An inbox agent usually needs:

  • list_messages, get_message, and get_thread to understand mail;
  • create_draft for work a person should approve;
  • reply_to_message only when autonomous sending is intentional.

Tool filtering improves model behavior. API-key permissions are the security boundary: if a key lacks message_send, a prompt cannot talk its way around the resulting 403.

3. Wake it when mail arrives

For production, expose an HTTP route and register it once:

npx carlyemail webhook https://your-agent.example/hooks/carlyemail \
  --events message.received

Save the whsec_... secret it prints — it is shown once.

In Python, do not write the route by hand. There are six decisions in it and they are the same six every time:

Python

from carlyemail.inbound import create_email_router
from fastapi import FastAPI

app = FastAPI()


async def on_email(email):
    await your_agent(email.thread_id, email.text)


app.include_router(
    create_email_router(
        on_email,
        path="/hooks/carlyemail",
        allow_from=["you@example.com"],
    )
)

That verifies the signature over the unmodified raw body, admits message.received and not the spam, blocked or unauthenticated variants, drops mail the inbox sent itself, checks the sender, ignores redeliveries, and answers the request before your agent starts thinking. decide() is the same logic with no framework in it, for everything that is not FastAPI — see receiving mail.

In another language, the rules are the ones in that table: verify the raw bytes, match the event type exactly, and treat delivery as at least once.

Use the event's thread_id as the agent-runtime conversation key. A reply then has two kinds of continuity:

  • CarlyEmail preserves the real email thread through In-Reply-To and References headers.
  • Your runtime can preserve private working memory under the same thread_id.

Use a WebSocket instead while developing locally or when the agent is already a long-running process. Use a webhook for serverless and durable runtimes because it wakes them and retries without an open connection.

4. Pick your framework

If your framework is not listed, it needs only one of these two interfaces:

  • an MCP client or function-tool API for actions;
  • an HTTP handler or WebSocket client for incoming events.

That is the entire adapter.

Before you let it send unattended

Email content is untrusted model input. Verify the CarlyEmail webhook, then still treat the sender's text and attachments as untrusted. Start with a key that can read and create drafts but cannot send. Add autonomous sending only after you have sender allowlists, loop protection, idempotency, and an escalation path. See human in the loop.