CarlyEmail docs

OpenAI Agents SDK

Give an OpenAI Agents SDK agent email tools and invoke it from signed incoming-mail events.

The OpenAI Agents SDK turns ordinary Python functions into model tools. CarlyEmail's Python SDK handles the email API, while a signed webhook starts the agent on each new message.

1. Install and create an inbox

pip install carlyemail openai-agents fastapi svix uvicorn
npx carlyemail signup --human-email you@example.com --username assistant
npx carlyemail verify 123456
export OPENAI_API_KEY=sk-...
export CARLYEMAIL_API_KEY=ce_us_...
export CARLYEMAIL_INBOX=assistant@carlyemail.com
export CARLYEMAIL_WEBHOOK_SECRET=whsec_...

2. Give the agent email tools

Python

import os

from agents import Agent, function_tool
from carlyemail import CarlyEmail

carly = CarlyEmail()
INBOX = os.environ["CARLYEMAIL_INBOX"]


@function_tool
def list_messages(limit: int = 20) -> list[dict]:
    """List recent messages in the agent's own inbox."""
    return carly.messages.list(INBOX, limit=limit)["messages"]


@function_tool
def read_message(message_id: str) -> dict:
    """Read one message in full, including its body."""
    return carly.messages.get(INBOX, message_id)


@function_tool
def reply(message_id: str, text: str) -> dict:
    """Reply in the same thread. This sends real email and cannot be undone."""
    return carly.messages.reply(INBOX, message_id, {"text": text})


inbox_agent = Agent(
    name="Inbox",
    instructions=(
        "Read the message and thread before acting. Never invent facts. "
        "Draft rather than send when intent or authority is ambiguous."
    ),
    tools=[list_messages, read_message, reply],
)

Only expose the operations the agent needs. Pair that model-visible list with an inbox-scoped API key, so a missing permission still returns 403 if a tool or guardrail is later misconfigured.

3. Invoke it when mail arrives

Python

import json

from agents import Runner
from fastapi import FastAPI, Request, Response
from svix.webhooks import Webhook, WebhookVerificationError

app = FastAPI()


@app.post("/hooks/carlyemail")
async def on_email(request: Request):
    raw = await request.body()
    try:
        event = Webhook(os.environ["CARLYEMAIL_WEBHOOK_SECRET"]).verify(
            raw, request.headers
        )
    except WebhookVerificationError:
        return Response("Invalid signature", status_code=400)

    message = event.get("message")
    if event.get("event_type") != "message.received" or not message:
        return Response(status_code=204)

    await Runner.run(
        inbox_agent,
        "Handle this newly received email. Reply if appropriate.\n\n"
        + json.dumps(message),
    )
    return Response(status_code=204)
uvicorn app:app --host 0.0.0.0 --port 8000
npx carlyemail webhook https://your-agent.example/hooks/carlyemail \
  --events message.received

The event's thread_id is the stable key to pass to your own session store when the agent needs private memory between messages. CarlyEmail preserves the real email headers when reply is called.

Handing off between agents

Email maps naturally onto SDK handoffs: a triage agent reads and routes, while specialists share the reply tool.

Python

billing = Agent(name="Billing", instructions="Answer billing questions.", tools=[reply])
support = Agent(name="Support", instructions="Answer product questions.", tools=[reply])

triage = Agent(
    name="Triage",
    instructions="Read new mail and hand off to the right specialist.",
    tools=[list_messages, read_message],
    handoffs=[billing, support],
)

Warning

Email content is untrusted model input. Verify the raw body, deduplicate by event_id, and start with draft-only permissions when a human should approve outbound mail.

See also