OpenAPI JSONDashboard

Guides

Inbox and Mail Flow

What this does#

Assmbl gives every agent (<agent_id>@<MAIL_DOMAIN>) a full email identity. Inbound mail flows SES → S3 → SQS → Lambda and is indexed in DynamoDB. Outbound sends use POST /send through SES.

When to use#

  • Your agent needs to receive instructions or data via email from humans or other agents.
  • You want to trigger automation when a message arrives (webhook or polling).
  • You need to reply, forward, or chain messages across multiple agents.

How to implement#

1. Send a message#

Python
from agentmail_client import AgentMailClient

client = AgentMailClient(
    base_url=os.environ["AGENTMAIL_API_BASE_URL"],
    token=os.environ["AGENTMAIL_TOKEN"],
)
client.send_mail(to="recipient@example.com", subject="Hello", body="Hi from Assmbl")

2. Poll the inbox#

Returns up to limit messages (max 100), oldest-first. Use cursor for pagination.

Python
page = client.list_inbox(limit=25)
for item in page.get("items", []):
    print(item["MessageID"], item["SenderVerified"])

Python SDK — all pages:

python
for page in client.iter_inbox_pages(page_size=50, verified_only=True):
    for item in page.get("items", []):
        process(item)

3. Fetch a full message (raw MIME)#

python
msg = client.get_message(message_id)
print(msg["raw_mime"])

4. Delete a message#

python
client.delete_message(message_id)

5. Idempotency#

Add X-AgentMail-Idempotency-Key to outbound SMTP to prevent duplicate side-effects:

text
X-AgentMail-Idempotency-Key: <your-unique-key>

The mail processor records the key and skips re-notification for duplicates.

6. Search the inbox#

Pass q to filter by subject, body preview, or sender (case-insensitive substring match). An empty string returns 400.

Python
page = client.list_inbox(q="invoice")

# Combined with other filters
for page in client.iter_inbox_pages(q="invoice", verified_only=True):
    for item in page.get("items", []):
        process(item)

7. Labels#

Labels let you tag messages for routing and filtering. Each label must be 1–64 characters; whitespace-only labels are rejected. A message may carry at most 20 labels.

Python
client.patch_message_labels(
    message_id,
    add=["needs-review", "urgent"],
    remove=["inbox"],
)

add and remove are applied atomically. Either field can be omitted or empty.

Python
page = client.list_inbox(label="needs-review")

8. Read/Unread tracking#

Mark a message as read or unread, and query the total unread count.

Python
client.patch_message_read(message_id, is_read=True)
client.patch_message_read(message_id, is_read=False)  # mark unread

Get unread count (excludes archived messages):

Python
result = client.get_unread_count()
print(result["unread_count"])

Filter inbox to unread only:

python
page = client.list_inbox(is_read=False)

9. Archive#

Archiving hides a message from the default inbox view without deleting it. The message is still accessible via GET /messages/{id} and appears when include_archived=true is set.

Python
client.patch_message_archive(message_id, archived=True)
client.patch_message_archive(message_id, archived=False)  # unarchive
Python
page = client.list_inbox(include_archived=True)

Attempting to archive a message that was already archived at the storage level returns 409.

Sender trust#

Every inbox item carries SenderVerified (true/false).

ValueMeaningRecommended action
trueSender passed verification for this agentNormal automation path
falseUnverified external senderLow-trust queue; avoid irreversible side effects

Filter to verified-only traffic:

Python
page = client.list_inbox(limit=25, verified_only=True)

Common failure modes#

SymptomLikely cause
404 on GET /messages/{id}Message was deleted, or wrong agent_id scope
Empty inbox despite sent mailSES receipt rule set not activated — run SesActivateCommand from stack outputs
SenderVerified always falseSENDER_VERIFICATION_ENABLED=0 or sender has not completed challenge
Duplicate processingMissing idempotency key; add X-AgentMail-Idempotency-Key
400 on GET /inbox?q=q must be non-empty when provided
400 on PATCH .../labelsLabel is empty, exceeds 64 chars, whitespace-only, or message already has 20 labels
409 on PATCH .../archiveMessage is already archived at the storage level and cannot be re-archived