Most apps that send email end up needing to attach something: a receipt, a generated PDF, a screenshot, a CSV export, a contract for signature. This recipe covers how to do it through the Nylas API.
The same endpoints work whether the mailbox belongs to a regular user grant or an Agent Account. The choice you do need to make is which encoding to use — JSON base64 for small files, multipart for everything else.
Pick an encoding
Section titled “Pick an encoding”Nylas accepts attachments two ways. Pick based on total request size:
| Total request size | Use | Why |
|---|---|---|
| Under 3MB | application/json with base64 | Simplest. Everything in one JSON body. |
| 3MB to 25MB | multipart/form-data | Avoids base64 inflation (~33%) and provider request limits. |
The 3MB threshold is the total request body, not just the file — subject, body HTML, and other fields count against it too. If you’re close to the limit, switch to multipart.
Before you begin
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key.
- A grant for the mailbox you’re sending from. This can be a normal user grant or an Agent Account grant.
- Send scope on the provider. Google needs
gmail.send. Microsoft needsMail.Send. Agent Accounts have what they need by default.
Option 1: JSON base64 (under 3MB)
Section titled “Option 1: JSON base64 (under 3MB)”Encode the file as base64 and include it in the attachments array on the send request.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "to": [{ "email": "[email protected]", "name": "Alex Customer" }], "subject": "Your Q3 invoice", "body": "<p>Invoice attached. Let us know if anything looks off.</p>", "attachments": [ { "filename": "invoice-q3.pdf", "content_type": "application/pdf", "content": "<BASE64_ENCODED_FILE>" } ] }'import fs from "fs";import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
const fileBuffer = fs.readFileSync("./invoice-q3.pdf");
const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: "Your Q3 invoice", body: "<p>Invoice attached. Let us know if anything looks off.</p>", attachments: [ { filename: "invoice-q3.pdf", contentType: "application/pdf", content: fileBuffer.toString("base64"), }, ], },});import base64from nylas import Client
nylas = Client("<NYLAS_API_KEY>")
with open("invoice-q3.pdf", "rb") as f: encoded = base64.b64encode(f.read()).decode("utf-8")
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": "Your Q3 invoice", "body": "<p>Invoice attached. Let us know if anything looks off.</p>", "attachments": [ { "filename": "invoice-q3.pdf", "content_type": "application/pdf", "content": encoded, } ], },)require 'nylas'require 'base64'
nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
encoded = Base64.strict_encode64(File.read('invoice-q3.pdf'))
sent, _ = nylas.messages.send( identifier: '<NYLAS_GRANT_ID>', request_body: { subject: 'Your Q3 invoice', body: '<p>Invoice attached. Let us know if anything looks off.</p>', attachments: [ { filename: 'invoice-q3.pdf', content_type: 'application/pdf', content: encoded, } ] })import com.nylas.NylasClient;import com.nylas.models.*;import com.nylas.util.FileUtils;import java.util.ArrayList;import java.util.List;
public class SendWithAttachment { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
CreateAttachmentRequest attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf"); List<CreateAttachmentRequest> attachments = new ArrayList<>(); attachments.add(attachment);
List<EmailName> to = new ArrayList<>();
SendMessageRequest requestBody = new SendMessageRequest.Builder(to) .subject("Your Q3 invoice") .body("<p>Invoice attached. Let us know if anything looks off.</p>") .attachments(attachments) .build();
Response<Message> sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody); System.out.println(sent.getData()); }}import com.nylas.NylasClientimport com.nylas.models.*import com.nylas.util.FileUtils
fun main() { val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
val attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf")
val requestBody = SendMessageRequest.Builder(to) .subject("Your Q3 invoice") .body("<p>Invoice attached. Let us know if anything looks off.</p>") .attachments(listOf(attachment)) .build()
val sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody) println(sent.data)}Each entry in attachments needs filename, content_type, and content (the base64 string). You can attach multiple files in the same array.
Option 2: multipart/form-data (up to 25MB)
Section titled “Option 2: multipart/form-data (up to 25MB)”For larger files, switch to multipart/form-data. The message JSON goes in the message form part; each attachment is a separate form part. The Nylas SDKs ship a FileUtils / file_utils helper that reads the file, sets the correct MIME type, and lets the SDK handle the multipart encoding — you almost never need to assemble the boundary yourself.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: multipart/form-data' \ --form 'message={ "to": [{ "email": "[email protected]", "name": "Alex Customer" }], "subject": "Your Q3 invoice", "body": "<p>Invoice attached. Let us know if anything looks off.</p>" }' \ --form 'invoice-q3.pdf=@/path/to/invoice-q3.pdf'import fs from "fs";import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
// Above 3MB, the Node SDK requires a Buffer or readable stream — base64 strings// are only supported for smaller payloads.const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: "Your Q3 invoice", body: "<p>Invoice attached. Let us know if anything looks off.</p>", attachments: [ { filename: "invoice-q3.pdf", contentType: "application/pdf", content: fs.createReadStream("./invoice-q3.pdf"), }, ], },});from nylas import Client, utils
nylas = Client("<NYLAS_API_KEY>")
attachment = utils.file_utils.attach_file_request_builder("invoice-q3.pdf")
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": "Your Q3 invoice", "body": "<p>Invoice attached. Let us know if anything looks off.</p>", "attachments": [attachment], },)require 'nylas'
nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
attachment = Nylas::FileUtils.attach_file_request_builder('invoice-q3.pdf')
sent, _ = nylas.messages.send( identifier: '<NYLAS_GRANT_ID>', request_body: { subject: 'Your Q3 invoice', body: '<p>Invoice attached. Let us know if anything looks off.</p>', attachments: [attachment], })import com.nylas.NylasClient;import com.nylas.models.*;import com.nylas.util.FileUtils;import java.util.ArrayList;import java.util.List;
public class SendMultipartAttachment { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
CreateAttachmentRequest attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf"); List<CreateAttachmentRequest> attachments = new ArrayList<>(); attachments.add(attachment);
List<EmailName> to = new ArrayList<>();
SendMessageRequest requestBody = new SendMessageRequest.Builder(to) .subject("Your Q3 invoice") .body("<p>Invoice attached. Let us know if anything looks off.</p>") .attachments(attachments) .build();
Response<Message> sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody); System.out.println(sent.getData()); }}import com.nylas.NylasClientimport com.nylas.models.*import com.nylas.util.FileUtils
fun main() { val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
val attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf")
val requestBody = SendMessageRequest.Builder(to) .subject("Your Q3 invoice") .body("<p>Invoice attached. Let us know if anything looks off.</p>") .attachments(listOf(attachment)) .build()
val sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody) println(sent.data)}Multipart avoids the base64 size inflation entirely and lets you push files up to 25MB without hitting either Nylas or provider request limits.
Inline images
Section titled “Inline images”Inline images are attachments that render inside the message body — a logo, an inline screenshot, a chart — referenced from the HTML by their content_id via <img src="cid:...">.
To send an inline image:
- Decide on a
content_idvalue. It must be alphanumeric and unique within the message (e.g.logo-2025). - Reference it from the body using
<img src="cid:logo-2025">. - Attach the file with
content_idset to the same value.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "to": [{ "email": "[email protected]" }], "subject": "Welcome aboard", "body": "<p>Welcome! <img src=\"cid:logo-2025\" alt=\"Our logo\" /></p><p>Excited to have you with us.</p>", "attachments": [ { "filename": "logo.png", "content_type": "image/png", "content": "<BASE64_ENCODED_PNG>", "content_id": "logo-2025" } ] }'import fs from "fs";import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
const logoBytes = fs.readFileSync("./logo.png");
const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: "Welcome aboard", body: '<p>Welcome! <img src="cid:logo-2025" alt="Our logo" /></p><p>Excited to have you with us.</p>', attachments: [ { filename: "logo.png", contentType: "image/png", content: logoBytes.toString("base64"), contentId: "logo-2025", isInline: true, }, ], },});from nylas import Client, utils
nylas = Client("<NYLAS_API_KEY>")
attachment = utils.file_utils.attach_file_request_builder("logo.png")attachment["content_id"] = "logo-2025"attachment["is_inline"] = True
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": "Welcome aboard", "body": '<p>Welcome! <img src="cid:logo-2025" alt="Our logo" /></p><p>Excited to have you with us.</p>', "attachments": [attachment], },)require 'nylas'
nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
attachment = Nylas::FileUtils.attach_file_request_builder('logo.png')attachment[:content_id] = 'logo-2025'attachment[:is_inline] = true
sent, _ = nylas.messages.send( identifier: '<NYLAS_GRANT_ID>', request_body: { subject: 'Welcome aboard', body: '<p>Welcome! <img src="cid:logo-2025" alt="Our logo" /></p><p>Excited to have you with us.</p>', attachments: [attachment], })import com.nylas.NylasClient;import com.nylas.models.*;import com.nylas.util.FileUtils;import java.io.FileInputStream;import java.nio.file.Files;import java.nio.file.Path;import java.util.ArrayList;import java.util.List;
public class SendInlineImage { public static void main(String[] args) throws Exception { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
Path file = Path.of("logo.png"); CreateAttachmentRequest attachment = new CreateAttachmentRequest.Builder( "logo.png", "image/png", new FileInputStream(file.toFile()), (int) Files.size(file) ) .contentId("logo-2025") .isInline(true) .build();
List<CreateAttachmentRequest> attachments = new ArrayList<>(); attachments.add(attachment);
List<EmailName> to = new ArrayList<>();
SendMessageRequest requestBody = new SendMessageRequest.Builder(to) .subject("Welcome aboard") .body("<p>Welcome! <img src=\"cid:logo-2025\" alt=\"Our logo\" /></p><p>Excited to have you with us.</p>") .attachments(attachments) .build();
Response<Message> sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody); System.out.println(sent.getData()); }}import com.nylas.NylasClientimport com.nylas.models.*import java.io.Fileimport java.io.FileInputStream
fun main() { val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
val file = File("logo.png") val attachment = CreateAttachmentRequest.Builder( filename = "logo.png", contentType = "image/png", content = FileInputStream(file), size = file.length().toInt(), ) .contentId("logo-2025") .isInline(true) .build()
val requestBody = SendMessageRequest.Builder(to) .subject("Welcome aboard") .body("<p>Welcome! <img src=\"cid:logo-2025\" alt=\"Our logo\" /></p><p>Excited to have you with us.</p>") .attachments(listOf(attachment)) .build()
val sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody) println(sent.data)}When content_id is set on an attachment, Nylas treats it as inline and most mail clients render it in place. If the receiving client can’t render inline images, the file falls back to being shown as a regular attachment.
Reply with an attachment
Section titled “Reply with an attachment”When you reply in-thread, attach files the same way. Combine the steps with reply_to_message_id:
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "reply_to_message_id": "<MESSAGE_ID>", "to": [{ "email": "[email protected]" }], "subject": "Re: Your Q3 invoice", "body": "<p>Signed copy attached.</p>", "attachments": [ { "filename": "invoice-q3-signed.pdf", "content_type": "application/pdf", "content": "<BASE64_ENCODED_FILE>" } ] }'import fs from "fs";import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
const signedBytes = fs.readFileSync("./invoice-q3-signed.pdf");
const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { replyToMessageId: "<MESSAGE_ID>", subject: "Re: Your Q3 invoice", body: "<p>Signed copy attached.</p>", attachments: [ { filename: "invoice-q3-signed.pdf", contentType: "application/pdf", content: signedBytes.toString("base64"), }, ], },});from nylas import Client, utils
nylas = Client("<NYLAS_API_KEY>")
attachment = utils.file_utils.attach_file_request_builder("invoice-q3-signed.pdf")
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "reply_to_message_id": "<MESSAGE_ID>", "subject": "Re: Your Q3 invoice", "body": "<p>Signed copy attached.</p>", "attachments": [attachment], },)require 'nylas'
nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
attachment = Nylas::FileUtils.attach_file_request_builder('invoice-q3-signed.pdf')
sent, _ = nylas.messages.send( identifier: '<NYLAS_GRANT_ID>', request_body: { reply_to_message_id: '<MESSAGE_ID>', subject: 'Re: Your Q3 invoice', body: '<p>Signed copy attached.</p>', attachments: [attachment], })import com.nylas.NylasClient;import com.nylas.models.*;import com.nylas.util.FileUtils;import java.util.ArrayList;import java.util.List;
public class ReplyWithAttachment { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
CreateAttachmentRequest attachment = FileUtils.attachFileRequestBuilder("invoice-q3-signed.pdf"); List<CreateAttachmentRequest> attachments = new ArrayList<>(); attachments.add(attachment);
List<EmailName> to = new ArrayList<>();
SendMessageRequest requestBody = new SendMessageRequest.Builder(to) .replyToMessageId("<MESSAGE_ID>") .subject("Re: Your Q3 invoice") .body("<p>Signed copy attached.</p>") .attachments(attachments) .build();
Response<Message> sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody); System.out.println(sent.getData()); }}import com.nylas.NylasClientimport com.nylas.models.*import com.nylas.util.FileUtils
fun main() { val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
val attachment = FileUtils.attachFileRequestBuilder("invoice-q3-signed.pdf")
val requestBody = SendMessageRequest.Builder(to) .replyToMessageId("<MESSAGE_ID>") .subject("Re: Your Q3 invoice") .body("<p>Signed copy attached.</p>") .attachments(listOf(attachment)) .build()
val sent = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody) println(sent.data)}For the full thread-reply workflow, see Reply to an email thread.
The Agent Accounts variant
Section titled “The Agent Accounts variant”For Agent Accounts, the endpoints and request shape are identical. Substitute the agent’s grant ID and the same JSON or multipart code works. A few practical notes specific to agents:
- Agents often send attachments from generated content, not files on disk. A common pattern is: agent calls an external service to generate a PDF (a contract, a report, an extracted invoice), receives the bytes in memory, encodes them as base64, and sends. The file never touches the filesystem.
tracking_optionsworks on outbound from agent grants too. If your agent wants to know whether the recipient opened the email or clicked a link in it, settracking_optionson the send request. See Email tracking.- One outbound per logical reply. Agents that get retried — webhook redelivery, queue retries, manual replays — can easily send the same attachment twice. Dedupe at the agent level before calling send. See Prevent duplicate agent replies.
Things to know
Section titled “Things to know”- Replacing attachments on a draft is destructive. If you’re updating an existing draft and pass
attachments, the array replaces the existing list completely. To keep an existing file and add a new one, include both in the new array. - Filename can include the original path on some clients but Nylas strips it. Send the bare filename (
invoice.pdf), not a path (/tmp/uploads/invoice.pdf). Most providers ignore paths, but a few mail clients display them awkwardly. - Special characters in filenames cause provider rejections. Stick to ASCII, hyphens, underscores, and dots. If the file is from user input, sanitize before sending.
content_typematters for rendering. PDFs sent asapplication/octet-streamshow up as generic downloads instead of previewing inline in Gmail. Set the right MIME type so recipients see proper previews.- Some providers cap individual attachments below 25MB. Gmail allows 25MB per message total but routes very large attachments through Drive links. Outlook limits vary by mailbox tier. If you regularly hit these limits, check the large attachments flow for Microsoft.
- Watch for accidental forwarding of inline images. When users forward an email, inline images sometimes detach and become regular attachments. Don’t rely on
is_inlinestaying stable across forwards.
What’s next
Section titled “What’s next”- Download attachments — the inbound side: reading and saving attachments off inbound messages
- Reply to an email thread — threading, picking the right message, and reply-with-attachment
- Using the Attachments API — the canonical reference
- Send Message API reference — endpoint spec
- Attachments API reference — endpoint spec