Send and download email attachments with Nylas
Two ways to attach a file when sending
There are two schemas for putting a file on an outbound message, and they're split by size. The JSON schema base64-encodes the file into an attachments array on a normal POST /messages/send request, which is the simple path. The multipart/form schema breaks the request into parts, each with its own MIME type, and carries the file as binary rather than base64. You pick based on how big the file is.
The size limits are the deciding factor and the first thing to get straight. The JSON method is capped at a 3MB total request payload, and the multipart method raises that to 25MB including the body content. So a small PDF or image goes in JSON without a second thought, and anything heading toward a few megabytes belongs in multipart. Beyond 25MB there's a separate flow covered later, but for everyday attachments the choice is JSON for small, multipart for large.
The 3MB limit is on the whole request
The single most common attachment bug is a file that's comfortably under 3MB failing to send anyway, and it comes down to what the limit measures. The 3MB cap on the JSON method is the entire HTTP request, not just the attachment content, so your message body, headers, recipient list, and every other attachment all count toward it. A 2.9MB file plus a long HTML body can push the request over the line.
Base64 makes this tighter than it looks. Encoding a binary file as base64 inflates it by roughly a third, so a 2.2MB file becomes about 3MB of encoded text before you've added anything else. The practical rule is to treat the JSON method as comfortable up to around 2MB of actual file content and switch to multipart well before the raw file approaches 3MB. If you're seeing sends rejected for size on files that seem small enough, the base64 inflation is almost always why.
Send a file with the JSON method
For a small file, you add an attachments array to the send request, where each entry carries the base64 content, a content_type, and a filename. The rest of the message (to, subject, body) is exactly as you'd send without an attachment, so adding a file is one more field rather than a different request shape.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"to": [{ "email": "c*****@example.com" }],
"subject": "Your invoice",
"body": "<p>Invoice attached.</p>",
"attachments": [{
"filename": "invoice.pdf",
"content_type": "application/pdf",
"content": "<BASE64_ENCODED_FILE>"
}]
}'
The content field is the file read and base64-encoded, which your language's standard library does in a line. Each attachment is its own object in the array, so a message can carry several, as long as the whole request stays under 3MB. For anything larger, the same attachments data moves into a multipart request instead, where the file rides as binary and the ceiling jumps to 25MB.
Embed an inline image with content_id
An inline image - a logo in a signature or a chart in the body - isn't a separate attachment at the bottom of the message; it's rendered in the HTML where you place it. The mechanism is content_id. You give the attachment a content_id, then reference it in the body's HTML with a cid: URL, and the image renders at that spot instead of hanging off the message as a downloadable file.
# In the attachment object:
"content_id": "logo123"
# In the body HTML:
<img src="cid:logo123">
The link is the matching identifier: the cid:logo123 in the <img> tag points at the attachment whose content_id is logo123. Get that pairing right and the image appears in the message body; get it wrong, or omit the content_id, and the same file shows up as a separate attachment instead. This is exactly how a branded signature with a logo works, and it's the piece people miss when an inline image stubbornly attaches itself to the bottom of the email.
Files over 25MB: the large attachments flow
When a file is too big even for multipart, there's a separate path, with a provider caveat. For Microsoft grants, Nylas supports attachments up to 150MB through a beta large attachments flow: you upload the file once, then reference its returned ID in your send request rather than inlining the bytes. It decouples the upload from the send, so a big file doesn't have to fit inside a single message request.
The constraint to know is that this flow is Microsoft-only. If you're sending from a Microsoft (Outlook or Exchange Online) grant and your file is between 25MB and 150MB, this is the path; for other providers, 25MB via multipart is the ceiling.
There's also a provider-storage wrinkle worth knowing: Google and Microsoft both have cloud-drive services, and a file shared from Google Drive or OneDrive usually appears as a link in the message body rather than as an attachment on the message object, so a "missing" attachment is sometimes a drive link instead.
Download an attachment from a received message
Pulling an attachment out of a message you received uses GET /v3/grants/{grant_id}/attachments/{attachment_id}/download, and the detail that trips people up is that message_id is a required query parameter. An attachment isn't a free-floating object; it belongs to the message it arrived on, so you pass both the attachment's ID and the message's ID to fetch it. The same message_id requirement applies to the metadata endpoint that returns an attachment's content_type, size, and filename.
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/attachments/<ATTACHMENT_ID>/download?message_id=<MESSAGE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--output invoice.pdf
The reason for the message scope is that the same physical file can appear on more than one message, and the attachment ID alone doesn't pin down which one you mean. So you carry the message_id from when you listed the message's attachments through to the download call. Forget it and the request fails for a missing required parameter, which is the single most common attachment-download error.
Inspect and download attachments from the CLI
The CLI handles the receive side, and it's the fast way to get a file out of a message without writing a request.
nylas email attachments list <message-id>shows every attachment on a message with its ID.nylas email attachments download <attachment-id> <message-id>pulls one down, saving it under its original filename or to a path you set with--output.nylas email attachments show <attachment-id> <message-id>returns the metadata.
# List attachments on a message, then download one
nylas email attachments list <message-id>
nylas email attachments download <attachment-id> <message-id> --output ./invoice.pdf
Notice the CLI mirrors the API's message scoping: download and show take both the attachment ID and the message ID, while list takes just the message ID, for the same reason the download endpoint requires message_id. One thing the CLI deliberately doesn't do is send attachments - there's no attachment flag on nylas email send - because attaching files is a structured-payload job that belongs in the API's JSON or multipart request. So the split is clean: send attachments with the API, and list, inspect, and download received ones from the terminal.
Where attachments work fits
The same send-and-receive handling sits under a range of features, and the size limits shape which path each takes. A few that map straight on:
- Sending generated documents. Invoices, receipts, and reports rendered to PDF go out on the JSON method while they're small, moving to multipart as they grow.
- Branded email with inline images. A logo or header image embedded via
content_idand acid:reference renders in the body, not as a clutter attachment. - Processing inbound files. A workflow that receives documents downloads each attachment with its
message_idand feeds it to parsing, storage, or a model. - Large file delivery on Microsoft. Contracts or media up to 150MB go through the large attachments flow on Microsoft grants rather than failing the 25MB multipart ceiling.
Each is the same handful of primitives: an attachments array or multipart part to send, the download endpoint to receive, with the file size choosing the path.
Things to keep in mind
A short list of details keeps attachments predictable.
- The 3MB JSON limit is the whole request. Body, headers, and every attachment count, and base64 inflates a file by about a third, so switch to multipart well before the raw file nears 3MB.
- Multipart raises the ceiling to 25MB. Use it for anything beyond a small file; it carries the file as binary instead of base64.
- Inline images need
content_idpluscid:. Match the body'scid:reference to the attachment'scontent_id, or the image attaches instead of embedding. - Downloads require
message_id. An attachment is scoped to its message; pass both IDs to the download and metadata endpoints. - Over 25MB is Microsoft-only. The large attachments flow reaches 150MB on Microsoft grants; other providers cap at 25MB.
- The CLI receives, not sends.
nylas email attachmentslists and downloads;nylas email sendhas no attachment flag, so sending files is the API's job.
Wrapping up
Attachments come down to size and scope. To send, base64 a small file into the attachments array under the 3MB whole-request limit, or move to multipart for up to 25MB, and embed inline images by matching a content_id to a cid: reference in the body. To receive, download from GET /v3/grants/{grant_id}/attachments/{attachment_id}/download with the required message_id, or run nylas email attachments download from the terminal. Remember that base64 inflation makes the 3MB limit tighter than it reads, that inline images live or die by the content ID, and that files past 25MB need the Microsoft-only large attachments flow.
Where to go next
- Working with attachments - JSON and multipart schemas in depth
- Send large attachments - the Microsoft 150MB flow
- Send a message - the attachments field and send options
- Nylas CLI email commands -
nylas email attachments list,show, anddownload
AI-answer pages for agents
When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:
- Topic runbook: https://cli.nylas.com/ai-answers/send-email-with-attachments-api-agent.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
Comments
No comments yet. Start the discussion.