Docs · SDKs

Python SDK

Sync and async clients for Python 3.9+, fully typed, with httpx as the only dependency.

shell
pip install capumattu

The package has a sync client (Capumattu) and an async one (AsyncCapumattu). Both read MAILROOM_API_KEY and MAILROOM_URL (default https://mailroom.capumattu.com) and work as context managers. Responses are plain dicts described by TypedDicts.

agent.py
from capumattu import Capumattu

with Capumattu() as mr:  # reads MAILROOM_API_KEY and MAILROOM_URL
    inbox = mr.inboxes.create(
        username="support-bot",
        display_name="Support Bot",
        client_id="support-bot",  # idempotent
    )
    print(inbox["address"])  # support-bot@agents.capumattu.com

    # Wait for the next email (long-poll, up to 60 s). None on timeout.
    msg = mr.messages.wait(inbox["id"], timeout=60)
    if msg:
        print(msg["extracted_text"])  # the new part of the reply only
        mr.messages.reply(inbox["id"], msg["id"], text="Thanks, on it.")

Temporary inboxes: mr.inboxes.create_temporary(ttl_seconds=900). Custom domains: mr.domains.create/list/get/verify/update/delete, and mr.inboxes.create(domain=...).

#Async

async_agent.py
import asyncio
from capumattu import AsyncCapumattu


async def main() -> None:
    async with AsyncCapumattu() as mr:
        inbox = await mr.inboxes.create()
        msg = await mr.messages.wait(inbox["id"], timeout=30)


asyncio.run(main())

#Contacts, knowledge and drafts

The same resources as the TypeScript client, with snake_case names: mr.contacts.list/get/by_address/update, mr.inboxes.update, mr.threads.update, mr.knowledge.list/create/get/update/delete/search and mr.messages.draft_reply.

contacts.py
c = mr.contacts.by_address("priya.n@gmail.com")
print(c["notes"], c["metadata"].get("crm_id"))

mr.contacts.update(
    c["id"],
    notes=(c["notes"] or "") + "
Asked about order 1042 on 25 Sep.",
    metadata={"crm_id": "hs_48213", "plan": "wholesale"},
    add_labels=["customer"],
)
knowledge.py
mr.knowledge.create(
    title="Shipping times",
    body="- Germany: 1–2 working days (DHL)
- EU: 3–5 working days",
    inbox_id=inbox["id"],
)

hits = mr.knowledge.search("how long does shipping to France take", inbox_id=inbox["id"])
for h in hits["data"]:
    print(h["title"], h["rank"], h["snippet"])
draft.py
draft = mr.messages.draft_reply(msg["id"], instructions="Keep it short.")
print(draft["text"], [c["title"] for c in draft["citations"]])
# review it, then:
mr.messages.reply(msg["inbox_id"], msg["id"], text=draft["text"])

#Pagination

paginate.py
from capumattu import iter_all

for msg in iter_all(mr.messages.list, inbox["id"], is_read=False):
    print(msg["subject"])

#Errors

Non-2xx responses raise MailroomError (status, code, message), or a subclass: BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), UnprocessableEntityError (422), RateLimitError (429), InternalServerError (5xx). Network failures raise APIConnectionError with status 0.

#Webhooks

verify_webhook(signature, timestamp, raw_body, secret) returns False on bad input and never raises. See Webhooks for Flask and FastAPI examples.