# How to send emails with attachments

Source: https://developer.nylas.com/docs/cookbook/email/attachments/send-attachments/

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](/docs/v3/agent-accounts/). The choice you do need to make is which encoding to use — JSON base64 for small files, multipart for everything else.

> **Info:** 
> **This recipe covers attachments up to 25MB.** That covers the vast majority of real-world traffic. For Microsoft mailboxes sending up to 150MB, there's a separate beta [large attachments flow](/docs/v3/email/send-large-attachments/) — it's a two-step upload pattern not covered here.

## 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

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key.
- A [grant](/docs/v3/auth/) 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 needs `Mail.Send`. Agent Accounts have what they need by default.


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


## Option 1: JSON base64 (under 3MB)

Encode the file as base64 and include it in the `attachments` array on the send request.

```bash
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": "alex@example.com", "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>"
      }
    ]
  }'
```

```js [sendJsonAttachment-Node.js SDK]


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: {
    to: [{ email: "alex@example.com", name: "Alex Customer" }],
    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"),
      },
    ],
  },
});
```

```python [sendJsonAttachment-Python SDK]

from 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={
        "to": [{"email": "alex@example.com", "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": encoded,
            }
        ],
    },
)
```

```ruby [sendJsonAttachment-Ruby SDK]
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: {
    to: [{ email: 'alex@example.com', 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: encoded,
      }
    ]
  }
)
```

```java [sendJsonAttachment-Java SDK]


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<>();
    to.add(new EmailName("alex@example.com", "Alex Customer"));

    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());
  }
}
```

```kt [sendJsonAttachment-Kotlin SDK]


fun main() {
  val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")

  val attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf")

  val to = listOf(EmailName("alex@example.com", "Alex Customer"))

  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.

> **Info:** 
> **The SDK helpers handle the size threshold for you.** `FileUtils.attachFileRequestBuilder()` (Java/Kotlin), `Nylas::FileUtils.attach_file_request_builder` (Ruby), and `utils.file_utils.attach_file_request_builder` (Python) all read the file, pick the right encoding based on size, and build the attachment object. If you're attaching bytes from disk, use the helper. The base64-string form is most useful when you have the bytes in memory and never write them to disk — for example, generated PDFs or in-memory rendering.

> **Warn:** 
> **Base64 inflates the payload by about a third.** A 2.2MB file becomes a ~3MB base64 string, which pushes the total request over the 3MB JSON limit once you add the body and headers. If your file is over ~2MB, use multipart instead.

## 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.

```bash
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": "alex@example.com", "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'
```

```js [sendMultipartAttachment-Node.js SDK]


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: {
    to: [{ email: "alex@example.com", name: "Alex Customer" }],
    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"),
      },
    ],
  },
});
```

```python [sendMultipartAttachment-Python SDK]
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={
        "to": [{"email": "alex@example.com", "name": "Alex Customer"}],
        "subject": "Your Q3 invoice",
        "body": "<p>Invoice attached. Let us know if anything looks off.</p>",
        "attachments": [attachment],
    },
)
```

```ruby [sendMultipartAttachment-Ruby SDK]
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: {
    to: [{ email: 'alex@example.com', name: 'Alex Customer' }],
    subject: 'Your Q3 invoice',
    body: '<p>Invoice attached. Let us know if anything looks off.</p>',
    attachments: [attachment],
  }
)
```

```java [sendMultipartAttachment-Java SDK]


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<>();
    to.add(new EmailName("alex@example.com", "Alex Customer"));

    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());
  }
}
```

```kt [sendMultipartAttachment-Kotlin SDK]


fun main() {
  val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")

  val attachment = FileUtils.attachFileRequestBuilder("invoice-q3.pdf")

  val to = listOf(EmailName("alex@example.com", "Alex Customer"))

  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

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:

1. Decide on a `content_id` value. It must be alphanumeric and unique within the message (e.g. `logo-2025`).
2. Reference it from the body using `<img src="cid:logo-2025">`.
3. Attach the file with `content_id` set to the same value.

```bash
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": "alex@example.com" }],
    "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"
      }
    ]
  }'
```

```js [inlineImage-Node.js SDK]


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: {
    to: [{ email: "alex@example.com" }],
    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,
      },
    ],
  },
});
```

```python [inlineImage-Python SDK]
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={
        "to": [{"email": "alex@example.com"}],
        "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],
    },
)
```

```ruby [inlineImage-Ruby SDK]
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: {
    to: [{ email: 'alex@example.com' }],
    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],
  }
)
```

```java [inlineImage-Java SDK]


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<>();
    to.add(new EmailName("alex@example.com", null));

    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());
  }
}
```

```kt [inlineImage-Kotlin SDK]


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 to = listOf(EmailName("alex@example.com", null))

  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

When you reply in-thread, attach files the same way. Combine the steps with `reply_to_message_id`:

```bash
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": "alex@example.com" }],
    "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>"
      }
    ]
  }'
```

```js [replyAttach-Node.js SDK]


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>",
    to: [{ email: "alex@example.com" }],
    subject: "Re: Your Q3 invoice",
    body: "<p>Signed copy attached.</p>",
    attachments: [
      {
        filename: "invoice-q3-signed.pdf",
        contentType: "application/pdf",
        content: signedBytes.toString("base64"),
      },
    ],
  },
});
```

```python [replyAttach-Python SDK]
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>",
        "to": [{"email": "alex@example.com"}],
        "subject": "Re: Your Q3 invoice",
        "body": "<p>Signed copy attached.</p>",
        "attachments": [attachment],
    },
)
```

```ruby [replyAttach-Ruby SDK]
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>',
    to: [{ email: 'alex@example.com' }],
    subject: 'Re: Your Q3 invoice',
    body: '<p>Signed copy attached.</p>',
    attachments: [attachment],
  }
)
```

```java [replyAttach-Java SDK]


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<>();
    to.add(new EmailName("alex@example.com", null));

    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());
  }
}
```

```kt [replyAttach-Kotlin SDK]


fun main() {
  val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")

  val attachment = FileUtils.attachFileRequestBuilder("invoice-q3-signed.pdf")

  val to = listOf(EmailName("alex@example.com", null))

  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](/docs/cookbook/email/threads/reply-to-a-thread/).

## 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_options` works on outbound from agent grants too.** If your agent wants to know whether the recipient opened the email or clicked a link in it, set `tracking_options` on the send request. See [Email tracking](/docs/v3/email/message-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](/docs/cookbook/agent-accounts/prevent-duplicate-replies/).

## 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_type` matters for rendering.** PDFs sent as `application/octet-stream` show 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](/docs/v3/email/send-large-attachments/) 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_inline` staying stable across forwards.

## What's next

- [Download attachments](/docs/cookbook/email/attachments/download-attachments/) — the inbound side: reading and saving attachments off inbound messages
- [Reply to an email thread](/docs/cookbook/email/threads/reply-to-a-thread/) — threading, picking the right message, and reply-with-attachment
- [Using the Attachments API](/docs/v3/email/attachments/) — the canonical reference
- [Send Message API reference](/docs/reference/api/messages/send-message/) — endpoint spec
- [Attachments API reference](/docs/reference/api/attachments/) — endpoint spec