Messages

Send mail from an agent and read its mailbox.

Messages are scoped to an agent. The same routes accept either the master key (acting on any agent) or the agent's own per-agent key (scoped to itself).

Send a message

  • Method: POST
  • Path: /agents/:id/messages/send
  • Auth: master key, or this agent's own per-agent key

Request body

{
  "to": ["alice@example.com"],
  "cc": ["bob@example.com"],
  "bcc": ["audit@example.com"],
  "subject": "Welcome to the beta",
  "text": "Plain text body.",
  "html": "<p>HTML body.</p>",
  "attachments": [
    {
      "filename": "report.pdf",
      "contentType": "application/pdf",
      "data": "JVBERi0xLjQKJ..."
    }
  ]
}
FieldTypeRequiredDescription
tostring | stringYesOne or more valid email addresses.
ccstring | stringNoCarbon-copy recipients.
bccstring | stringNoBlind-carbon-copy recipients (never appear in the composed MIME).
subjectstringYes1–998 chars.
textstringConditionalRequired if html is omitted. Min 1 char.
htmlstringConditionalRequired if text is omitted. Min 1 char.
attachmentsobjectNoUp to 10 entries. Each: filename (1–255), contentType (1–255), data (base64-encoded, ≤ ~5 MiB raw).

Limits:

  • Total to+cc+bcc ≤ 50 recipients.
  • Request body ≤ 1 MiB UTF-8 bytes.
  • Composed MIME ≤ 25 MiB (the Cloudflare Send Email provider cap).
  • From is set to the agent's own address; user-supplied From is ignored.

Response 202 Accepted (status sent or partial)

{
  "id": "0f7d4b73-7a7f-4f6b-8a18-2a4e5e6c9c0a",
  "status": "sent",
  "message_id_header": "<uuid@agents.yourdomain.com>",
  "recipients": [
    { "recipient": "alice@example.com", "status": "sent" }
  ]
}

Response 502 Bad Gateway (status rejected)

{
  "id": "0f7d4b73-7a7f-4f6b-8a18-2a4e5e6c9c0a",
  "status": "rejected",
  "message_id_header": "<uuid@agents.yourdomain.com>",
  "error": "E_SENDER_NOT_VERIFIED",
  "recipients": [
    { "recipient": "alice@example.com", "status": "rejected", "error": "E_SENDER_NOT_VERIFIED" }
  ]
}

Status semantics

Aggregate statusMeaning
sentEvery envelope recipient succeeded.
partialSome recipients succeeded, others failed. The Worker continues attempting later recipients after a single failure. The message.sent webhook still fires.
rejectedEvery recipient failed. No webhook fires.

The per-recipient outcome is also persisted to the per-agent SQLite message_recipients table so future retries can target only the failed subset.

Errors

  • 400 — Zod validation failed (invalid email, missing subject, no body, too many recipients, oversized attachment, bad base64, body too large, MIME exceeds 25 MiB).
  • 401 — missing or bad bearer.
  • 403 — per-agent key for the wrong agent.
  • 404 — agent unknown or tombstoned.

List messages

  • Method: GET
  • Path: /agents/:id/messages
  • Auth: master key, or this agent's own per-agent key

Query parameters

ParameterTypeDefaultDescription
limitinteger50Clamped to 1..100.
offsetinteger0Pagination offset.

Response 200 OK

{
  "messages": [
    {
      "id": "uuid",
      "direction": "inbound",
      "from_addr": "alice@example.com",
      "to_addr": "abc123def456@agents.yourdomain.com",
      "subject": "Hi",
      "status": "received",
      "raw_size": 1234,
      "created_at": 1730000000,
      "thread_id": "uuid"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

The list endpoint deliberately omits body_text / body_html / message_id_header for compactness. Fetch the full record via GET /agents/:id/threads/:threadId.

Errors

  • 401, 403, 404 as above.

Message schema

FieldTypeDescription
idstring (uuid)Per-agent message id.
direction"inbound" | "outbound"
from_addrstringEnvelope sender.
to_addrstringStored recipient (for outbound, the joined envelope recipients).
subjectstring | null
statusstringreceived, sent, partial, rejected, pending.
raw_sizeinteger | nullUTF-8 byte size of the raw MIME (outbound) or the parsed email (inbound).
created_atintegerUnix timestamp (seconds).
thread_idstring | nullThe conversation this message belongs to.
message_id_headerstring | nullRFC 5322 Message-ID (only on the per-thread GET /threads/:id).
body_textstring | nullPlain text body (only on the per-thread GET /threads/:id).
body_htmlstring | nullHTML body (only on the per-thread GET /threads/:id).