# Receive email (/tasks/receive)

<!-- agent-signals: reading_time_min: 3 · est_tokens: 1476 · updated: 2026-08-03 -->
Related: [Send and reply](/tasks/send.md), [Manage conversations](/tasks/conversations.md), [Control who can email your agent](/tasks/inbound-control.md), [How AgentMail is architected](/architecture.md)



# Receive email with AgentMail

Poll an AgentMail inbox for inbound threads, reply content, attachments, and raw `.eml` files. Webhook and WebSocket receiving are separate.

## Do this

```bash
export AGENTMAIL_API_KEY="your-key"
BASE="https://api.agentmail.to/v0"
AUTH="Authorization: Bearer $AGENTMAIL_API_KEY"

agentmail inboxes create --username support --domain agentmail.to
agentmail threads list
agentmail threads get --thread-id "$THREAD_ID"
agentmail threads get-attachment \
  --thread-id "$THREAD_ID" \
  --attachment-id "$ATTACHMENT_ID"
agentmail inboxes:messages get-raw \
  --inbox-id "$INBOX_ID" \
  --message-id "$MESSAGE_ID"

curl -X PATCH "$BASE/inboxes/$INBOX_ID/messages/$MESSAGE_ID" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"add_labels":["read"],"remove_labels":["unread"]}'
```

## SDK

Install: `npm install agentmail` (TypeScript) or `pip install agentmail` (Python). Clients: `new AgentMailClient({ apiKey })` / `AgentMail(api_key=...)`.

| Operation      | TypeScript                                                                        | Python                                                                                        |
| -------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Create inbox   | `client.inboxes.create({ username, domain })`                                     | `client.inboxes.create(request=CreateInboxRequest(username=..., domain=...))`                 |
| List threads   | `client.threads.list({ labels, limit, pageToken })`                               | `client.threads.list(labels=[...], limit=..., page_token=...)`                                |
| Get thread     | `client.threads.get(threadId)`                                                    | `client.threads.get(thread_id=...)`                                                           |
| Get attachment | `client.threads.getAttachment(threadId, attachmentId)`                            | `client.threads.get_attachment(thread_id, attachment_id)`                                     |
| Get raw `.eml` | `client.inboxes.messages.getRaw(inboxId, messageId)`                              | `client.inboxes.messages.get_raw(inbox_id, message_id)`                                       |
| Update labels  | `client.inboxes.messages.update(inboxId, messageId, { addLabels, removeLabels })` | `client.inboxes.messages.update(inbox_id, message_id, add_labels=[...], remove_labels=[...])` |

Python requires `request=CreateInboxRequest(...)` for `inboxes.create`; import `CreateInboxRequest` from `agentmail.inboxes`.

Reference: [https://docs.agentmail.to/api-reference](https://docs.agentmail.to/api-reference)

## Facts

* Base URL: `https://api.agentmail.to/v0`. Auth: `Authorization: Bearer <api key>`.
* `inbox_id` is `username@domain`.
* Inbox creation accepts optional `username`, `domain`, `display_name`, and `client_id`. The default domain is `agentmail.to`; custom domains must be verified, or be an enabled subdomain of one.
* Inbound messages receive `received` and `unread`; classification can add `spam`, `unauthenticated`, or `blocked`.
* Present but failing SPF, DKIM, or DMARC causes the email to be dropped. Missing headers deliver the email with `unauthenticated`.
* `GET /v0/threads` returns `{count, limit?, next_page_token?, threads[]}` in descending `timestamp` order. `GET /v0/inboxes/{inbox_id}/threads` scopes to one inbox.
* Listings hide spam, unauthenticated, blocked, and trashed threads by default. Use the relevant `include_*` flag and matching label-read permission.
* Thread filters `senders`, `recipients`, and `subject` use word-prefix matching, cap `limit` at 100, and do not match arbitrary substrings. Filter addresses by username prefix; digit-only values return `400`.
* `GET /v0/threads/{thread_id}` returns `messages[]` in ascending `timestamp` order.
* `text` and `html` are full bodies. Optional `extracted_text` and `extracted_html` omit quoted history. `text` can be absent for HTML-only email.
* Attachment and raw-email endpoints return a signed `download_url` and `expires_at`; raw email also returns `size`.
* Attachment endpoint: `GET /v0/inboxes/{inbox_id}/messages/{message_id}/attachments/{attachment_id}`. Raw endpoint: `GET /v0/inboxes/{inbox_id}/messages/{message_id}/raw`.
* Mark processed with `PATCH /v0/inboxes/{inbox_id}/messages/{message_id}` and `{"remove_labels":["unread"]}`.

## Not supported

* Python `client.inboxes.create()` does not accept flat `username=` or `domain=` arguments.
* Filters do not support arbitrary substrings; full literal addresses can return no results.
* Error bodies have no top-level `code`. Field-level snake\_case codes occur only in a 400 response's `errors[]`.
* `received` cannot be changed through the API.
* Dropped authentication failures cannot be retrieved.
* Attachment metadata has no bytes or download URL. Signed URLs are temporary.

## Errors

| Name              | HTTP | Body               | Fix                                                           |
| ----------------- | ---- | ------------------ | ------------------------------------------------------------- |
| `NotFoundError`   | 404  | `{name, message}`  | Verify the ID, label-read permissions, and `include_*` flags. |
| `ValidationError` | 400  | `{name, errors[]}` | Read each entry's path, message, and snake\_case code.        |

Branch on HTTP status and PascalCase `name`. Only `name` is present in every error body: 404 has `message`; 400 detail is in `errors[]`.

## Verify

```bash
agentmail threads list
```

Success returns threads visible to the API key. An empty organization returns `count: 0` and an empty `threads` list.

## Related

* [Quickstart](/quickstart)
* [API reference](https://docs.agentmail.to/api-reference)


## Tenant instructions

Every page ships two renditions at one URL. The HTML page is for humans. The Markdown rendition (append .md to any page URL, or request with Accept: text/markdown) is agent-optimized: a Do this section with runnable commands, SDK signatures, exhaustive Facts, a Not supported section listing shapes that do NOT work, an Errors table, and a Verify command. Prefer the Markdown rendition over scraping HTML, and trust Not supported entries instead of retrying those call shapes.