Forwarding an email means sending the original message to a new set of recipients, with the original content quoted below your note and the attachments carried along. There’s no dedicated forward field in the v3 API, and that surprises people. You compose a new message yourself, set fresh to recipients, prefix the subject with Fwd:, and include the original body and files.
This recipe shows the full forward composition against /messages/send: how to fetch the original message, build a quoted body, re-attach files, and why forwarding differs from a reply. The same flow works whether a person triggers it in your UI or an automated process forwards an inbound message to a downstream team.
How forwarding differs from replying
Section titled “How forwarding differs from replying”Forwarding and replying both call POST /v3/grants/{grant_id}/messages/send, but they set different fields. A reply uses reply_to_message_id to thread under the original conversation and usually goes back to the original sender. A forward sends the original content to brand-new recipients you choose, so you leave threading off and compose the quoted content into the body yourself.
Per the Messages API reference, reply_to_message_id is the ID of the message you’re replying to. For Gmail and Microsoft Graph it’s the provider message ID, and for IMAP it’s the RFC822 Message-ID header. The API reads that message and stamps In-Reply-To and References headers so the outbound nests in the thread. That’s exactly what you don’t want when forwarding, because a forward starts a fresh conversation with people who weren’t on the original. So for a true forward you omit reply_to_message_id, set new to recipients, and put the quoted original into the body. If you need a reply instead, see How to reply to an email thread.
Fetch the original message
Section titled “Fetch the original message”Before you can forward an email, you need its content: the subject, the body, the sender, and the list of attachments. Fetch the message by ID with GET /v3/grants/{grant_id}/messages/{message_id}. The response includes an attachments array where each of the 4 core fields per file (id, filename, content_type, and size in bytes) describes the file, but not the file bytes themselves.
curl --compressed --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json'{ "request_id": "5fa64c92-e840-4357-86b9-2aaa1f8b6c1f", "data": { "id": "<MESSAGE_ID>", "grant_id": "<NYLAS_GRANT_ID>", "object": "message", "subject": "Q3 proposal next steps", "date": 1717942920, "body": "<p>Hi, here are the revised numbers we discussed...</p>", "attachments": [ { "id": "<ATTACHMENT_ID>", "grant_id": "<NYLAS_GRANT_ID>", "filename": "q3-proposal.pdf", "content_type": "application/pdf", "size": 84213 } ], "thread_id": "<THREAD_ID>" }}import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
async function fetchMessageById() { try { const message = await nylas.messages.find({ identifier: "<NYLAS_GRANT_ID>", messageId: "<MESSAGE_ID>", });
console.log("message:", message); } catch (error) { console.error("Error fetching message:", error); }}
fetchMessageById();from nylas import Client
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"message_id = "<MESSAGE_ID>"
message = nylas.messages.find( grant_id, message_id,)
print(message)The body field holds the original HTML, which you’ll quote in the forwarded message. The attachments array lists files by ID, but each file needs its own download before you can re-attach it, so a message with 2 attachments means 2 requests after this fetch. Keep the from, date, to, and subject values too, since a clean forward header repeats them at the top of the quoted body.
Compose and send the forward
Section titled “Compose and send the forward”A forward is a new message with new to recipients and a body that holds your note plus the quoted original. The request below sends through POST /v3/grants/{grant_id}/messages/send with the subject prefixed Fwd: and an HTML body that reproduces the original headers and content. There’s no reply_to_message_id, so the message starts a fresh conversation rather than threading.
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": "Ops Team" }], "subject": "Fwd: Q3 proposal next steps", "body": "<p>Forwarding this for your review.</p><hr><p><b>From:</b> Alex Customer <[email protected]><br><b>Date:</b> Mon, 9 Jun 2025 14:22:00<br><b>Subject:</b> Q3 proposal next steps</p><p>Hi, here are the revised numbers we discussed...</p>" }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
// `original` is the message you fetched in the previous step.const quoted = `<p>Forwarding this for your review.</p><hr>` + `<p><b>From:</b> ${original.from[0].name} <${original.from[0].email}><br>` + `<b>Subject:</b> ${original.subject}</p>` + original.body;
const subject = original.subject.startsWith("Fwd:") ? original.subject : `Fwd: ${original.subject}`;
const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject, body: quoted, },});from nylas import Client
nylas = Client("<NYLAS_API_KEY>")
# `original` is the message you fetched in the previous step.sender = original["from"][0]quoted = ( "<p>Forwarding this for your review.</p><hr>" f"<p><b>From:</b> {sender['name']} <{sender['email']}><br>" f"<b>Subject:</b> {original['subject']}</p>" f"{original['body']}")
subject = original["subject"]if not subject.startswith("Fwd:"): subject = f"Fwd: {subject}"
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": subject, "body": quoted, },)The API doesn’t add the subject prefix for you, so set Fwd: yourself and skip it when the original subject already starts with one. Build the quoted header from the original from, date, and subject so recipients see where the message came from. A body-only forward like this one stays well under the 3 MB JSON request limit, and attachments are what push you toward it. The new message lands in the sender’s Sent folder like any other outbound email.
Re-attach the original files
Section titled “Re-attach the original files”Attachments don’t travel automatically. The forwarded message only includes files you put in its attachments array, so you download each original file, then attach it to the send request. Download with GET /v3/grants/{grant_id}/attachments/{attachment_id}/download, which requires the message_id of the message the attachment belongs to as a query parameter.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/attachments/<ATTACHMENT_ID>/download?message_id=<MESSAGE_ID>' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --output original-attachment.pdfimport Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
// Download each attachment listed on the original message, then base64-encode it.const attachments = await Promise.all( original.attachments.map(async (file) => { const stream = await nylas.attachments.download({ identifier: "<NYLAS_GRANT_ID>", attachmentId: file.id, queryParams: { messageId: original.id }, }); const chunks = []; for await (const chunk of stream) chunks.push(chunk); return { filename: file.filename, contentType: file.content_type, content: Buffer.concat(chunks).toString("base64"), }; }));
const sent = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { subject: `Fwd: ${original.subject}`, body: quoted, attachments, },});import base64from nylas import Client
nylas = Client("<NYLAS_API_KEY>")
# Download each attachment listed on the original message, then base64-encode it.attachments = []for file in original["attachments"]: data = nylas.attachments.download( "<NYLAS_GRANT_ID>", file["id"], query_params={"message_id": original["id"]}, ) attachments.append({ "filename": file["filename"], "content_type": file["content_type"], "content": base64.b64encode(data).decode("utf-8"), })
sent = nylas.messages.send( "<NYLAS_GRANT_ID>", request_body={ "subject": f"Fwd: {original['subject']}", "body": quoted, "attachments": attachments, },)Base64-encoded attachments in a JSON request work up to 3 MB total. For files larger than that and up to 25 MB, switch to multipart/form-data, which the send attachments recipe covers step by step. Watch out for inline images: if the original body references a cid: source, re-attach that file with a matching content_id so the image renders instead of breaking.
Things to watch for
Section titled “Things to watch for”A few details trip people up when they build forwarding on top of /messages/send. Most stem from the fact that you compose the forward yourself rather than calling a single forward endpoint, so you re-add every piece of the original you want carried over. The size limit is the most common surprise: an original message with a 10 MB attachment can’t ride along in a base64 JSON request.
- Don’t set
reply_to_message_idon a forward. That field threads the message under the original conversation and references the original sender. A forward goes to new recipients and should start fresh, so leave it out. Use it only when you mean to reply to the thread. - Sanitize the quoted body before you send. The original
bodyis raw provider HTML and can carry remote tracking images or brokencid:references. Strip or rewrite anything you don’t want forwarded, especially when an automated process forwards inbound mail from an unverified source. - Re-attach inline images with their
content_id. Inline images use acid:reference in the HTML and a matchingcontent_idon the attachment. Drop thecontent_idand the image shows as a broken link in the forwarded copy. - Carry CC and BCC with intent. A forward doesn’t copy the original
cclist. Addccorbcconly if you want those people on the new message, since forwarding to a fresh recipient set is usually the point. - Mind the total payload size. The base64 JSON path caps at 3 MB. If the combined attachments exceed it, switch to multipart or drop large files and link to them instead.
What’s next
Section titled “What’s next”- How to reply to an email thread: the threading path with
reply_to_message_idwhen you mean to reply, not forward - How to send emails with attachments: the base64 and multipart attachment patterns in detail
- Read a single message or thread: fetch the original message content before composing the forward
- Messages API reference: full reference for
/messages/send