Installation

terminal
pip install deadsimple-email

Quick Start

Create a client, make an inbox, and send your first email in four lines:

quickstart.py
from deadsimple import DeadSimple

client = DeadSimple(api_key="dse_your_api_key")
inbox = client.inboxes.create(display_name="My Agent")

client.messages.send(
    inbox_id=inbox.inbox_id,
    to="user@example.com",
    subject="Hello from my agent",
    text_body="This email was sent by an AI agent.",
)

Async Client

For async frameworks like FastAPI, use the async client. Every method from the sync client has an async equivalent:

async_example.py
from deadsimple import AsyncDeadSimple

client = AsyncDeadSimple(api_key="dse_your_api_key")

inbox = await client.inboxes.create(display_name="Async Agent")
messages = await client.messages.list(inbox_id=inbox.inbox_id)

Resources

Every API endpoint is available as a typed method on the client:

Resource Methods
client.inboxes create, list, get, delete, bulk_create, bulk_delete
client.messages send, list, get, reply, forward
client.threads list, get
client.webhooks create, list, get, update, delete (with inbox_ids scope)
client.api_keys create (with inbox_ids scope), list, get, delete
client.calendar create, list, list_all, get, update, delete, invite, rsvp, feed, rotate_feed
client.metrics get
client.domains add, list, verify, delete
client.identity sign_in, enroll_key, list_keys, delete_key, connections
client.usage get

Identity: sign an agent in to other apps

Any app that offers Sign in with Dead Simple accepts an inbox as a login over standard OpenID Connect. Hand the SDK the app's authorization URL and it completes the flow with the API key the client already holds:

identity.py
# authorization_url is the app's "Sign in with Dead Simple" link
# (its /authorize URL with PKCE, state and scopes already set).
result = client.identity.sign_in(inbox.inbox_id, authorization_url)
print(result["redirect_to"], result["final_url"], result["status"])
# follow=False returns redirect_to only, so you can drive the HTTP client yourself.

# Agents that should not hold a full API key: enrol a P-256 public key once,
# then sign a short-lived ES256 assertion at sign-in time.
key = client.identity.enroll_key(inbox.inbox_id, public_key=open("agent.pub.pem").read(), name="worker-7")
print(key["kid"])
client.identity.list_keys(inbox.inbox_id)      # shows last_used_at per key
client.identity.delete_key(inbox.inbox_id, key["kid"])

# Every app the inbox has signed in to, with scope and sign-in counts
client.identity.connections(inbox.inbox_id)

Pay per request. The SDK needs an API key, but the API itself does not: priced routes answer 402 with no credentials and accept USDC over x402 or a Stripe card over MPP inline, and the first paid inbox returns an API key you can then pass to DeadSimple(api_key=...). Prices, headers and an httpx-based x402 client are on the pay per request page.

Calendar: events, invitations, RSVPs and a feed

Every inbox has a calendar. Create events, send them as real iCalendar invitations, answer invitations that arrived by email, and hand a human the subscription URL. Times need a zone: a trailing Z, an offset, or timezone= next to a local time.

calendar.py
# Create, then invite. Nothing is sent until invite.
event = client.calendar.create(inbox.inbox_id, title="Kickoff",
                              start="2026-09-22T11:00:00", timezone="America/New_York",
                              duration_minutes=30, attendees=["dana@example.com"])
sent = client.calendar.invite(inbox.inbox_id, event["event_id"], text_body="Agenda to follow.")

# Reschedule: update raises sequence, invite again sends a revision, not a duplicate
client.calendar.update(inbox.inbox_id, event["event_id"], start="2026-09-23T11:00:00", timezone="America/New_York")
client.calendar.invite(inbox.inbox_id, event["event_id"])

# Invitations that arrived by email are already on the calendar (source == "inbound")
for ev in client.calendar.list(inbox.inbox_id, status="confirmed")["events"]:
    if ev["source"] == "inbound":
        client.calendar.rsvp(inbox.inbox_id, ev["event_id"], "accepted", comment="See you then.")

# Subscription URLs for Google Calendar, Apple Calendar or Outlook; rotate if the URL leaks
feed = client.calendar.feed(inbox.inbox_id)        # feed["feed_url"], feed["webcal_url"]
client.calendar.rotate_feed(inbox.inbox_id)

# Every event on every inbox the key can see
client.calendar.list_all(start_from="2026-09-21", start_to="2026-09-27")

Metrics: a time series for the account or an inbox

client.metrics.get() wraps GET /v1/metrics. Each bucket carries sent, received, bounced, complained, opened, clicked, webhook_deliveries, webhook_failures and injection_flagged, plus totals over the window. Windows span at most 92 days; hourly buckets are available for windows of seven days or less.

metrics.py
# Last 30 days, daily buckets (the defaults)
m = client.metrics.get()
print(m.totals.sent, m.totals.received, m.totals.bounced)

# One week, hourly, for two inboxes
m = client.metrics.get(from_="2026-09-10", to="2026-09-17", interval="hour",
                       inbox_ids=[support_id, billing_id])
for bucket in m.series:
    if bucket.webhook_failures or bucket.injection_flagged:
        print(bucket.start, bucket.webhook_failures, bucket.injection_flagged)

Inbox-scoped API keys and webhooks

Pass inbox_ids when creating an API key and the key can see and act on those inboxes only: any other inbox of the account answers 404, account-wide listings (inboxes, messages, threads, metrics) are filtered to them, and the key cannot create inboxes or mint a key with more reach than it has. Webhooks take the same field and fire only for events on those inboxes.

scoping.py
# A key for one agent, limited to its own inbox
key = client.api_keys.create("support-agent", permissions=["read", "write"],
                             inbox_ids=[support_id])
print(key.key)          # shown once

# A webhook that only hears that inbox
hook = client.webhooks.create("https://agents.example.com/hooks/support",
                              events=["message.received", "calendar.event.created"],
                              inbox_ids=[support_id])

# Change the scope later; inbox_ids=[] makes it account-wide again
client.webhooks.update(hook.webhook_id, inbox_ids=[support_id, billing_id])
client.webhooks.get(hook.webhook_id)

Framework Integrations

Install optional extras to get pre-built tool definitions for popular AI agent frameworks:

terminal
# LangChain toolkit
pip install deadsimple-email[langchain]

# CrewAI tools
pip install deadsimple-email[crewai]

# OpenAI Agents SDK
pip install deadsimple-email[openai-agents]

# MCP server
pip install deadsimple-email[mcp]

# All framework integrations
pip install deadsimple-email[all]

Each extra adds framework-specific tool definitions. See the individual integration pages for details:

Bulk Operations

Create or delete many inboxes in a single API call:

bulk.py
# Create 10 inboxes at once
inboxes = client.inboxes.bulk_create(
    count=10,
    display_name_prefix="Agent",
)

for inbox in inboxes:
    print(f"{inbox.email} ready")

# Clean up when done
ids = [inbox.inbox_id for inbox in inboxes]
client.inboxes.bulk_delete(inbox_ids=ids)

CLI Included

The Python SDK includes the dse command-line tool. Once installed, you can manage inboxes, send email, and check usage from your terminal:

terminal
export DSE_API_KEY=dse_your_api_key

dse inboxes list
dse send --inbox inb_xxx --to user@example.com --subject "Hello" --body "Hi there"
dse usage

See the full CLI documentation for all available commands.

Configuration

The client accepts the API key directly, or reads it from the DSE_API_KEY environment variable:

config.py
import os
from deadsimple import DeadSimple

# Explicit key
client = DeadSimple(api_key="dse_your_api_key")

# Or from environment (reads DSE_API_KEY automatically)
client = DeadSimple()

Ready to build?

Create a free account and start integrating in minutes.

API Reference Get Started Free