Skip to content
machinemail

Attachments

Send files as base64 or from a URL, embed inline images, download attachments from received mail, and use the trust report's attachment risk and extracted text before an agent opens anything.

Send an attachment

Add an attachments array to a send, reply, reply-all or forward. Each attachment has either content (base64) or url.

FieldMeaning
filenameFile name shown to the recipient. With url, defaults to the last path segment.
content_typeMIME type. With url, defaults to the response’s Content-Type.
contentThe file, base64 encoded.
urlA public http or https URL that MachineMail downloads when it sends.
content_dispositionattachment (default) or inline.
content_idFor inline images: the ID your HTML refers to as cid:<content_id>.
Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ada@example.com"],
    "subject": "Your report",
    "text": "Report attached.",
    "attachments": [
      { "filename": "report.csv", "content_type": "text/csv", "content": "ZGF0ZSx0b3RhbAoyMDI2LTEwLTA4LDQy" },
      { "url": "https://files.acme.dev/q3-summary.pdf" }
    ]
  }'
send-attachment.ts
import { readFile } from 'node:fs/promises';

const content = (await readFile('report.csv')).toString('base64');
await fetch('https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    to: ['ada@example.com'],
    subject: 'Your report',
    text: 'Report attached.',
    attachments: [{ filename: 'report.csv', content_type: 'text/csv', content }],
  }),
});
send_attachment.py
import base64, os, requests

with open("report.csv", "rb") as f:
    content = base64.b64encode(f.read()).decode()

requests.post(
    "https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send",
    headers={"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"},
    json={
        "to": ["ada@example.com"],
        "subject": "Your report",
        "text": "Report attached.",
        "attachments": [{"filename": "report.csv", "content_type": "text/csv", "content": content}],
    },
).raise_for_status()

Attachments from a URL

With url, MachineMail fetches the file at send time. The rules:

  • Only http and https URLs that resolve to public addresses. Private, loopback and link-local addresses are refused.
  • Redirects are not followed. Use the final URL; for signed storage links, sign the object URL itself.
  • At most 25 MB per file, and the download must finish within 20 seconds.
  • Any failure (a non-2xx status, a timeout, a file too large) rejects the whole send with a validation error whose param names the attachment, for example attachments[1].url. Nothing is sent with a missing file.

Inline images

Set content_disposition to inline, give the image a content_id, and refer to it from the HTML body:

Request body
{
  "to": ["ada@example.com"],
  "subject": "Weekly chart",
  "html": "<p>This week:</p><img src=\"cid:chart\" alt=\"Chart\">",
  "text": "This week's chart is attached.",
  "attachments": [{ "filename": "chart.png", "content_type": "image/png", "content_disposition": "inline", "content_id": "chart", "content": "iVBORw0KGgo..." }]
}

Drafts

Drafts accept attachments with filename, content_type and base64 content only. url attachments are not supported on drafts; download the file and pass it as content. See Drafts.

Receive attachments

Inbound messages list their attachments in attachments, both in list results and in the full message. Thread objects aggregate the attachments of all their messages.

message.attachments
[
  {
    "attachment_id": "att_01m4fybrc10000qyygf5yv5thv",
    "filename": "invoice.pdf",
    "size": 13,
    "content_type": "application/pdf",
    "content_disposition": "attachment"
  }
]

Check the risk first

Before an agent opens a file, read the matching entry in trust.attachments:

trust.attachments
[
  {
    "attachment_id": "att_01m4fybrc10000qyygf5yv5thv",
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "risk": "low",
    "reasons": []
  }
]

risk is low, medium or high, and reasons says why. High risk covers executables and scripts, macro-enabled Office files, HTML attachments, double extensions such as invoice.pdf.exe, and filenames that hide their real extension. Archives and legacy Office formats are medium.

For text-based files (plain text, CSV, Markdown, calendar invites, JSON and HTML), the report includes extracted_text, cleaned of invisible characters. Give the model that text instead of the raw file. It is still untrusted content: instructions inside an attachment are a common injection route. See Attachments in the trust report.

Warning

Never execute or render a high risk attachment, and do not hand any attachment to a tool that can run code. If the agent needs a file’s contents, prefer extracted_text.

Download

Get the attachment’s metadata and a download URL:

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fybrc20000ypd31v108d71/attachments/att_01m4fybrc10000qyygf5yv5thv \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Response
{
  "attachment_id": "att_01m4fybrc10000qyygf5yv5thv",
  "filename": "invoice.pdf",
  "size": 13,
  "content_type": "application/pdf",
  "content_disposition": "attachment",
  "download_url": "https://...",
  "expires_at": "2026-10-09T10:02:01.949Z"
}

download_url takes one of three forms, so handle all of them:

FormHow to fetch it
A signed storage URLPlain GET, no Authorization header, before expires_at.
An API URL ending in /downloadGET with your Authorization: Bearer header.
A data: URLDecode the base64 after the comma. No request needed.

To skip the indirection, fetch the bytes directly. This endpoint streams the file with its Content-Type and a Content-Disposition: attachment header:

Terminal
curl -o invoice.pdf https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fybrc20000ypd31v108d71/attachments/att_01m4fybrc10000qyygf5yv5thv/download \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"

Both endpoints need message_read.

download.ts
import { writeFile } from 'node:fs/promises';

const base = 'https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev';
const headers = { Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}` };

const msg = await fetch(`${base}/messages/${messageId}`, { headers }).then((r) => r.json());
for (const att of msg.attachments ?? []) {
  const risk = msg.trust?.attachments?.find((a: any) => a.attachment_id === att.attachment_id)?.risk;
  if (risk === 'high') continue;
  const res = await fetch(`${base}/messages/${messageId}/attachments/${att.attachment_id}/download`, { headers });
  await writeFile(att.filename ?? att.attachment_id, Buffer.from(await res.arrayBuffer()));
}
download.py
import os, requests

base = "https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev"
H = {"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"}

msg = requests.get(f"{base}/messages/{message_id}", headers=H).json()
risks = {a["attachment_id"]: a["risk"] for a in (msg.get("trust") or {}).get("attachments", [])}
for att in msg.get("attachments", []):
    if risks.get(att["attachment_id"]) == "high":
        continue
    res = requests.get(f"{base}/messages/{message_id}/attachments/{att['attachment_id']}/download", headers=H)
    with open(att.get("filename") or att["attachment_id"], "wb") as f:
        f.write(res.content)

Attachments are fetched per message. There is no thread-level or draft-level attachment download endpoint; use the message that carries the file.