Upload, attach, send, and retrieve files through a single API. MultiMail handles storage, presigned access, and MIME validation so your agent doesn't have to.
Attachments are a common failure point in agent-driven email systems. Files inflate request payloads, require separate storage backends, and introduce malware vectors that a naive API pass-through won't catch. Most teams bolt on S3, write custom MIME checks, and manually wire presigned URLs — none of which belongs in application logic. When an inbound email carries a .xlsx or a .pdf, the agent needs a safe, structured way to retrieve it without downloading raw bytes into memory or exposing a public URL.
MultiMail treats attachments as first-class objects. You upload a file to get an attachment ID, reference that ID when sending, and receive structured metadata on inbound emails. Downloads are issued as short-lived presigned URLs — agents never hold raw file bytes, and links expire automatically. MIME type filtering runs at ingestion, so you can allowlist .pdf, .xlsx, and .csv without writing validation code. The result is a consistent read/write pattern for files that works the same whether you're sending a report or parsing an inbound invoice.
POST the file to /v1/attachments as multipart/form-data. MultiMail stores it, runs MIME validation against your configured allowlist, and returns an attachment ID you reference in subsequent calls.
Include the attachment ID in the attachments array of a send_email call. The API links the file to the outbound message without re-uploading bytes. Multiple attachments map to multiple IDs in the same array.
Inbound emails with attachments trigger an email.received webhook payload that includes structured attachment metadata: filename, content_type, size_bytes, and attachment_id. No binary data in the webhook body.
Call fetch_presigned_url with the attachment ID to get a time-limited download URL. Default expiry is 3600 seconds. Pass the URL to downstream tools, storage systems, or the human approver without granting permanent access.
Configure per-mailbox MIME allowlists via the API or dashboard. Attachments whose content_type is not on the list are rejected at ingestion — the email is still delivered, but the attachment is quarantined and flagged in the webhook payload.
import multimail
client = multimail.Client(api_key="mm_live_...")
"cm"># Upload the file — returns an attachment object with .id
with open("quarterly-report.pdf", "rb") as f:
attachment = client.upload_attachment(
filename="quarterly-report.pdf",
content_type="application/pdf",
data=f.read(),
)
"cm"># Send with the attachment ID
message = client.send_email(
mailbox="[email protected]",
to="[email protected]",
subject="Quarterly report attached",
body="Hi Dana, I&"cm">#039;ve attached the Q1 report as a PDF. Use the secure link below to download it.",
attachments=[attachment.id],
)
print(message.id) "cm"># em_01HXYZ...Upload a file to get an attachment ID, then reference it in a send_email call.
"cm"># Step 1: Upload the attachment
ATTACHMENT=$(curl -s -X POST https://api.multimail.dev/v1/attachments \
-H "Authorization: Bearer $MULTIMAIL_API_KEY..." \
-F "[email protected]" \
-F "content_type=application/pdf")
ATTACHMENT_ID=$(echo $ATTACHMENT | jq -r &"cm">#039;.id')
echo "Uploaded: $ATTACHMENT_ID"
"cm"># Step 2: Send the email referencing the attachment ID
curl -s -X POST https://api.multimail.dev/v1/send_email \
-H "Authorization: Bearer $MULTIMAIL_API_KEY..." \
-H "Content-Type: application/json" \
-d "{
\"mailbox\": \"[email protected]\",
\"to\": \"[email protected]\",
\"subject\": \"Quarterly report attached\",
\"body\": \"Hi Dana, please find the Q1 report attached.\",
\"attachments\": [\"$ATTACHMENT_ID\"]
}"Two-step REST flow: upload the file, then pass the returned attachment ID into send_email.
import multimail
client = multimail.Client(api_key="mm_live_...")
"cm"># Read an inbound email (email_id comes from your webhook handler)
email = client.read_email(email_id="em_01HABC...")
for att in email.attachments:
print(f"{att.filename} — {att.content_type} — {att.size_bytes} bytes")
"cm"># Presigned URL expires in 1 hour
download = client.fetch_presigned_url(
attachment_id=att.id,
expires_in=3600,
)
print(f"Download URL: {download.url}")
"cm"># Pass download.url to your storage system, approver, or downstream toolRead an inbound email, iterate its attachments, and get a time-limited download URL for each.
from fastapi import FastAPI, Request
import multimail
app = FastAPI()
client = multimail.Client(api_key="mm_live_...")
@app.post("/webhooks/multimail")
async def handle_inbound(request: Request):
payload = await request.json()
if payload["event"] != "email.received":
return {"ok": True}
email_id = payload["data"]["email_id"]
attachments = payload["data"].get("attachments", [])
for att in attachments:
if att["quarantined"]:
"cm"># MIME type not on allowlist — skip or alert
print(f"Quarantined: {att[&"cm">#039;filename']} ({att['content_type']})")
continue
url = client.fetch_presigned_url(
attachment_id=att["id"],
expires_in=1800,
)
# Hand off url.url to your document processing pipeline
process_document(att["filename"], url.url)
return {"ok": True}Parse the email.received webhook payload and extract attachment metadata without downloading file bytes.
import multimail
client = multimail.Client(api_key="mm_live_...")
"cm"># Update mailbox to only accept PDFs, spreadsheets, and plain text
client.update_mailbox(
mailbox_id="mbx_01HDEF...",
attachment_allowlist=[
"application/pdf",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"text/csv",
"text/plain",
],
)
"cm"># Inbound attachments with content_type outside this list
"cm"># are quarantined — webhook payload will include quarantined: trueRestrict which file types a mailbox will accept as attachments. Anything outside the list is quarantined.
Attachments live in MultiMail storage. Your agent works with IDs and presigned URLs — never raw binary payloads in memory or API request bodies.
Configure an allowlist of permitted content types per mailbox. Executables, archives, and other unwanted types are quarantined at ingestion, not after your agent has already processed them.
The same attachment ID model works for both outbound (upload → reference) and inbound (receive metadata → fetch URL). One pattern covers the full file lifecycle.
Presigned URLs expire on a configurable timer (default 3600s). Downstream tools, human approvers, and external systems get temporary access without permanent storage exposure.
Every inbound attachment comes with filename, content_type, size_bytes, quarantine status, and ID in the webhook payload. Your agent can make routing decisions before ever fetching the file.
Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.