# Dead Simple Email: custom connector brief

This file is written for an AI agent that is about to build a connector to
Dead Simple Email. It was published for Meta Muse's "custom connector" flow,
but any agent that can make HTTPS requests can follow it. Read the whole file
before writing code. The full OpenAPI spec is at
https://deadsimple.email/openapi.yaml and a broader index is at
https://deadsimple.email/llms.txt.

## What this service is

Dead Simple Email gives an agent its own email inboxes. Each inbox is a real
mailbox with a real address. The agent can create inboxes, send mail from them,
read what arrives, reply in-thread, and pull one-time codes and magic links out
of signup emails. Nothing here touches the human's personal Gmail or Outlook.

## Connection details

- Base URL: `https://api.deadsimple.email`
- Auth: every request carries `Authorization: Bearer <DSE_API_KEY>`.
  Keys start with `dse_`. Store the key in your credential store and reference
  it by name; never print it, log it, or repeat it back to the user.
- Content type: `application/json` for request and response bodies.
- Optional `Idempotency-Key` header on POST requests. The same key within 24
  hours returns the cached response instead of re-executing. Use it on sends.
- Rate limits are reported on every response in `X-RateLimit-Limit`,
  `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A `429` carries
  `Retry-After` in seconds. Back off and retry; do not hammer.
- Errors are `{"error": {"code": "...", "message": "...", "details": [...]}}`.
  `401` means the key is missing or revoked. `402` means the account hit a plan
  limit. `404` means the inbox or message id is wrong.

The human gets a key at https://app.deadsimple.email/dashboard/settings/keys
(free plan, no card). Name the key after the agent so it can be revoked on its
own later.

## The eight calls a connector needs

### 1. Create an inbox

```
POST /v1/inboxes
{"display_name": "Muse", "local_part": "muse"}
```

All fields are optional. Omit `local_part` for a random address. On the shared
domain a short suffix is appended to the local part you ask for, so the address
comes back as `muse_ab12@box1.deadsimple.email` rather than `muse@...`. Pass
`"domain"` only if the account has verified a custom domain.

Response `201`:

```
{"inbox_id": "...", "email": "muse_ab12@box1.deadsimple.email",
 "display_name": "Muse", "status": "active", "created_at": "..."}
```

Keep `inbox_id` and `email` in memory. Every other call needs the `inbox_id`.
Create one inbox per project or persona, not one per message.

### 2. List inboxes

```
GET /v1/inboxes
```

Response `{"inboxes": [...], "count": n, "cursor": "..."}`. Use this on startup
to find an inbox you already created instead of creating another.

### 3. Send an email

```
POST /v1/inboxes/{inbox_id}/messages
{"to": ["person@example.com"], "subject": "Quote request",
 "text_body": "Hello ...", "html_body": "<p>Hello ...</p>"}
```

`to` and `subject` are required. Send at least `text_body`. Optional: `cc`,
`bcc`, `reply_to`, `send_at` (ISO 8601, schedules the send), `attachments`,
`in_reply_to` (a message id from this inbox or a raw Message-ID header, to
thread onto an existing conversation). Response `201`:
`{"message_id": "...", "status": "sent"}`.

### 4. List messages in an inbox

```
GET /v1/inboxes/{inbox_id}/messages?limit=20
```

Response `{"messages": [...], "count": n, "cursor": "..."}`. Each message has
`message_id`, `thread_id`, `direction` (`inbound` or `outbound`), `from_email`,
`from_name`, `to`, `subject`, `snippet`, `labels`, `is_spam`, `received_at`.
Pass `cursor` back to page. Bodies are not included here; fetch one message
for the body.

### 5. Read one message with its full body

```
GET /v1/inboxes/{inbox_id}/messages/{message_id}
```

Adds `text_body`, `html_body`, `latest_reply` (just the new text above the
quoted history), `in_reply_to`, `references`, `attachments` (with
`extracted_text` for PDFs, spreadsheets and documents) and `extracted`
(`verification_code`, `magic_link_url` when the mail contains one).

### 6. Reply in-thread

```
POST /v1/inboxes/{inbox_id}/messages/{message_id}/reply
{"text_body": "Thanks, that works. See you Tuesday."}
```

Threading headers, subject and recipients are handled for you. Prefer this over
a fresh send whenever you are answering something. `.../reply-all` and
`.../forward` (`{"to": [...]}`) exist alongside it.

### 7. Read a whole conversation

```
GET /v1/inboxes/{inbox_id}/threads
GET /v1/inboxes/{inbox_id}/threads/{thread_id}
```

The second call returns every message in the thread, oldest first, so you can
summarise a negotiation or a support exchange in one request.

### 8. Get a verification code or magic link

```
GET /v1/inboxes/{inbox_id}/verification?since=2026-09-11T17:00:00Z
```

Non-blocking. Returns `{"found": false}` until a matching email arrives, then
`{"found": true, "verification_code": "482913", "magic_link_url": "https://...",
"from_email": "...", "subject": "..."}`. Pass `since` as the moment you
submitted the signup form so an older code is never returned. Optional
`from_contains` narrows by sender. Poll every 5 seconds for up to two minutes.

## Recipes

Sign up for a service:
1. Create or reuse an inbox. Use its `email` on the signup form.
2. Note the timestamp, submit the form.
3. Poll `/verification?since=<timestamp>` until `found` is true.
4. Enter `verification_code`, or open `magic_link_url`.

Contact a third party and wait for the answer:
1. Send from the inbox. Keep the returned `message_id`.
2. Every few minutes, list messages with `direction=inbound` newer than the
   send, or read the thread by the sent message's `thread_id`.
3. Reply with call 6 so the conversation stays in one thread.

Let the human review before anything goes out:
- `POST /v1/inboxes/{inbox_id}/drafts` with the same body as a send stores a
  draft. The human approves it in the dashboard, or the agent sends it later
  with `POST .../drafts/{draft_id}/send`.
- `PUT /v1/inboxes/{inbox_id}/guardrails` with
  `{"require_draft_approval": true, "max_sends_per_hour": 20,
  "allowed_domains": ["example.com"]}` enforces this on the server, not just
  in the agent's good intentions.

Give the human an address they can forward things to:
- Tell the human the inbox `email`. Anything they forward or CC to it shows up
  in call 4 within seconds. No Gmail connection is needed for that.

## Rules of the road

- One inbox per purpose. Reuse it across sessions; look it up with call 2.
- Never delete an inbox unless the human asks in so many words
  (`DELETE /v1/inboxes/{inbox_id}` is permanent).
- Do not send to anyone the human has not named or clearly implied.
- Say which address you sent from when you report back.
- If the API answers `402`, tell the human the plan limit was reached and stop.
- The free plan includes 5 inboxes and 5,000 emails a month.

## Optional extras

- IMAP/SMTP credentials for any inbox: `GET /v1/inboxes/{inbox_id}/credentials`.
- Server-sent stream of new inbound mail for agents that cannot host a webhook:
  `GET /v1/inboxes/{inbox_id}/stream?api_key=...` (`text/event-stream`).
- Webhooks: `POST /v1/webhooks` with `{"url": "...", "events": ["message.received"]}`.
- Contacts, templates, labels, calendar invites and custom domains are all in
  the OpenAPI spec.

## Also available as MCP

If your host speaks Model Context Protocol, skip the custom connector and add
`https://api.deadsimple.email/mcp` (streamable HTTP) with the same bearer
header. It exposes 14 tools covering everything above.
