The standard Send Message endpoint accepts attachments as base64-encoded content inside the JSON body, which caps the entire request at 3 MB (or 25 MB using multipart form data). For files larger than that, use the attachment-uploads API to upload the file directly to Nylas-managed storage, then reference it by ID when you send the message. This flow supports attachments up to 150 MB.
Supported providers: Microsoft (Outlook and Exchange Online via Microsoft Graph).
Google, IMAP, Yahoo, iCloud, and EWS grants are rejected with 401 Unauthorized. For these providers, use the standard attachment flow with its 25 MB multipart limit.
Before you begin
Section titled “Before you begin”You need the following before sending large attachments:
- A Microsoft grant with at least one of these scopes:
Mail.Send,Mail.ReadWrite, orMail.ReadWrite.Shared. See Create an Azure app for how to configure the auth app. - An Exchange Online tenant with message size limits raised to accommodate your largest attachment (see below).
Raise the Exchange Online message size limit
Section titled “Raise the Exchange Online message size limit”Exchange Online defaults to a 35 MB maximum message size. If your attachment plus message overhead exceeds this, the send fails and recipients on Exchange-based mailboxes do not receive the message.
Raise the limit using Exchange Online PowerShell as an Exchange administrator. Set the value to match your expected maximum attachment size. Changes can take up to 60 minutes to propagate.
Per-mailbox (recommended for a targeted rollout):
Organization-wide:
Set-TransportConfig -MaxSendSize 150MB -MaxReceiveSize 150MBHow it works
Section titled “How it works”Instead of embedding the file in the send request, you:
- Create an upload session. Nylas returns a direct-upload URL.
- Upload the file bytes.
PUTthe file directly to the returned URL. No Nylas auth header required. - Complete the session. Nylas verifies the upload and marks the attachment as ready.
- Send the email. Reference the
attachment_idin theattachmentsarray of your send request.
Step 1: Create an upload session
Section titled “Step 1: Create an upload session”Make a Create Upload Session request to start the flow.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<GRANT_ID>/attachment-uploads' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY_OR_ACCESS_TOKEN>' \ --data-raw '{ "filename": "document.pdf", "content_type": "application/pdf", "size": 1048576 }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
async function createUploadSession() { try { const session = await nylas.attachments.createUploadSession({ identifier: "<NYLAS_GRANT_ID>", requestBody: { filename: "quarterly-report.pdf", contentType: "application/pdf", size: 5242880, }, });
console.log("Upload session:", session); } catch (error) { console.error("Error creating upload session:", error); }}
createUploadSession();import requests
api_key = "<NYLAS_API_KEY>"grant_id = "<NYLAS_GRANT_ID>"api_uri = "https://api.us.nylas.com"
response = requests.post( f"{api_uri}/v3/grants/{grant_id}/attachment-uploads", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json", }, json={ "filename": "quarterly-report.pdf", "content_type": "application/pdf", "size": 5242880, },)response.raise_for_status()session = response.json()["data"]print(session){ "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88", "data": { "attachment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "method": "PUT", "url": "https://storage.googleapis.com/upload/storage/v1/b/BUCKET/o?uploadType=resumable&upload_id=...", "headers": { "Content-Type": "application/pdf" }, "expires_at": "2026-04-22T19:00:00Z", "max_size": 157286400, "size": 5242880, "content_type": "application/pdf", "filename": "quarterly-report.pdf", "grant_id": "abc123" }}| Field | Required | Notes |
|---|---|---|
filename | Yes | Name of the file as it appears in the email. |
content_type | Yes | MIME type (for example, application/pdf, image/png). Any non-empty string is accepted. |
size | No | Expected file size in bytes. Recommended. When provided, Nylas verifies the uploaded bytes match this value during completion. Maximum: 157286400 (150 MB). |
Save the attachment_id from the response. You need it for steps 3 and 4.
Step 2: Upload the file
Section titled “Step 2: Upload the file”PUT the file bytes to the url returned in step 1. This is a Google Cloud Storage resumable upload URL, so no Nylas authorization header is required on this request.
Set Content-Type to match the value you declared in step 1. A successful upload returns HTTP 200 or 201.
curl -X PUT "{url from step 1}" \ -H "Content-Type: application/pdf" \ -H "Content-Length: 5242880" \ --data-binary @quarterly-report.pdfimport { readFile } from "node:fs/promises";
const file = await readFile("quarterly-report.pdf");
const response = await fetch(session.url, { method: session.method, // "PUT" headers: { ...session.headers, "Content-Length": String(file.length), }, body: file,});
if (!response.ok) { throw new Error(`Upload failed: ${response.status} ${response.statusText}`);}import requests
with open("quarterly-report.pdf", "rb") as f: file_bytes = f.read()
response = requests.put( session["url"], headers={ **session["headers"], "Content-Length": str(len(file_bytes)), }, data=file_bytes,)response.raise_for_status()Step 3: Complete the upload session
Section titled “Step 3: Complete the upload session”Once the file is uploaded, notify Nylas so it can verify the upload and mark the attachment ready to send. See the Complete Upload Session reference.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<GRANT_ID>/attachment-uploads/<ATTACHMENT_ID>/complete' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY_OR_ACCESS_TOKEN>'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
async function completeUploadSession() { try { const result = await nylas.attachments.completeUploadSession({ identifier: "<NYLAS_GRANT_ID>", attachmentId: "<ATTACHMENT_ID>", });
console.log("Completed:", result); } catch (error) { console.error("Error completing upload session:", error); }}
completeUploadSession();import requests
api_key = "<NYLAS_API_KEY>"grant_id = "<NYLAS_GRANT_ID>"attachment_id = "<ATTACHMENT_ID>"api_uri = "https://api.us.nylas.com"
response = requests.post( f"{api_uri}/v3/grants/{grant_id}/attachment-uploads/{attachment_id}/complete", headers={ "Authorization": f"Bearer {api_key}", "Accept": "application/json", },)response.raise_for_status()result = response.json()["data"]print(result){ "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88", "data": { "attachment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "grant_id": "abc123", "status": "ready" }}The request body is empty. Once status is ready, the attachment can be referenced in a send request.
Step 4: Send the email
Section titled “Step 4: Send the email”Pass the attachment_id as id inside the attachments array of your Send Message or Create Draft request. Do not include content or content_type. Nylas fetches the file from storage and attaches it at send time.
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": [{ "name": "Jane Smith", "email": "[email protected]" }], "subject": "Q1 Report", "body": "Please find the report attached.", "attachments": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ] }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
const message = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: "Q1 Report", body: "Please find the report attached.", // Reference the uploaded attachment by ID. The CreateAttachmentRequest // type expects content + filename for inline uploads, but the API also // accepts a bare { id } reference for upload-session attachments. attachments: [{ id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } as any], } as any,});
console.log(message);from nylas import Client
nylas = Client( api_key="<NYLAS_API_KEY>",)
message = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": "Q1 Report", "body": "Please find the report attached.", "attachments": [{"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}], },)
print(message)If the referenced attachment is not ready at send time (for example, it is still uploading, has failed, or has expired), the send fails. Nylas deletes the draft in both the synchronous and use_draft=true scheduled paths, and the synchronous send returns a 500 to the caller.
Inline attachments
Section titled “Inline attachments”To embed a large image or file inline in the email body, set content_id on the attachment entry and reference it in the HTML body using a cid: URI.
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": [{ "name": "Jane Smith", "email": "[email protected]" }], "subject": "Q1 Report", "body": "<p>See chart below:</p><img src=\"cid:chart1\">", "attachments": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "content_id": "chart1" } ] }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: "Q1 Report", body: '<p>See chart below:</p><img src="cid:chart1">', attachments: [ { id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", contentId: "chart1", } as any, ], } as any,});from nylas import Client
nylas = Client(api_key="<NYLAS_API_KEY>")
nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": "Q1 Report", "body": '<p>See chart below:</p><img src="cid:chart1">', "attachments": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "content_id": "chart1", } ], },)Nylas treats the attachment as inline when the content_id value appears as cid:{content_id} inside the message body. If you include a content_id but the body does not reference it, the file is still attached but is displayed as a regular attachment rather than inline. See Working with inline attachments for the full rules.
Upload session status
Section titled “Upload session status”An upload session moves through one of the following statuses during its lifecycle.
| Status | Meaning |
|---|---|
uploading | Session created; Nylas is waiting for the file upload. |
ready | Upload verified and complete. The attachment can be referenced in a send or draft. |
failed | Upload initiation failed (for example, Nylas could not create the storage URL during session creation). Create a new session. |
expired | Session was not completed within 1 hour. Create a new session. |
The Create Upload Session response returns the initial state. The Complete Upload Session response returns ready once Nylas has verified the upload.
Errors
Section titled “Errors”All error responses follow the standard Nylas error shape:
{ "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88", "error": { "type": "invalid_request", "message": "file_size exceeds maximum allowed size of 157286400 bytes" }}| HTTP status | When it occurs |
|---|---|
400 Bad Request | filename or content_type is missing, size is negative or zero, size exceeds 150 MB, or the request body is not valid JSON. |
401 Unauthorized | Grant cannot be resolved, the provider is not Microsoft, the grant lacks the required Microsoft scopes, or the grant_id in the path does not match the session on the complete endpoint. |
404 Not Found | The attachment_id does not exist. |
409 Conflict | /complete was called on a session that is already ready or has failed. |
410 Gone | /complete was called on an expired session, or the session was already past expires_at. |
422 Unprocessable Entity | The file is not in storage, the uploaded size does not match the declared size, or no size was declared and the object is empty. |
500 Internal Server Error | Storage or database failure. Retry the operation. |
Rate limiting is not enforced at the attachment-uploads service itself. Any 429 Too Many Requests response is returned by the upstream API gateway. See Sending errors for general troubleshooting.
Limits and retention
Section titled “Limits and retention”| Parameter | Limit |
|---|---|
| Maximum file size | 150 MB (157286400 bytes) |
| Session expiry | 1 hour from creation |
| Supported providers | Microsoft (Graph) |
| Exchange Online default message size | 35 MB. Raise via PowerShell (see Before you begin). |
File retention after ready | Until expires_at (1 hour after creation). Send before the window closes. |
| Metadata retention | 60 days. The session record is kept for observability after the file is deleted. |
Related
Section titled “Related”- Using the Attachments API. Standard attachment flow (up to 3 MB JSON or 25 MB multipart), inline attachments, and downloading attachments.
- Sending messages with Nylas. End-to-end guide to sending and scheduling messages.
- Sending errors. Troubleshooting delivery failures.
- Create an Azure app. Set up the Microsoft auth app and scopes needed to authenticate a grant.
- API reference: Create an upload session, Complete upload session.