The Nylas Email API lets you send messages on behalf of authenticated users through their own email provider. You can either send a message immediately or create a draft and send it later. Both approaches use the same REST API, and the provider sees the activity as the user sending the message directly.
Before you begin
Section titled “Before you begin”Before you start sending messages, you need the following prerequisites:
- A Nylas application.
- A working authentication configuration. Either…
- A Google or Microsoft grant with at least the following scopes:
- Google:
gmail.send - Microsoft:
Mail.ReadWriteandMail.Send
- Google:
If you haven’t set up your Nylas application yet, complete the Getting Started guide first.
Choose how to send
Section titled “Choose how to send”Start with who the email is from. This page covers sending from a grant: a connected user’s mailbox or an Agent Account. To send from your own domain without a mailbox, use Transactional Send. To send automatically when an event happens, use a workflow.
Choose who the email comes from
Diagram as text
- Your app: You need to send an email: Pick whose address it comes from. Whichever you choose, the API response tells you straight away whether Nylas accepted the message, and Nylas can notify your app later about how the send turned out.
- Ends in one of:
- It should come from your user → Your user's own mailbox: For email a real person would send, like a sales rep's follow-up or a recruiter's interview invite. Recipients see your user's address and reply to them.
- It should come from your AI agent → An Agent Account: For an AI agent that needs its own email address, such as a support or scheduling assistant, so it can send and receive email with anyone.
- It should come from your product → Your company's domain: For email your product sends itself, like receipts, password resets, and alerts, from an address such as [email protected]. This is transactional send.
- It should go out automatically → A workflow: For email that goes out whenever something happens, like a booking confirmation, without your app sending each one. It sends from your user's mailbox or your company's domain.
From a grant, you can send a message in three ways:
When a grant's message goes out
send_at when it must go out later. message.send_success and message.send_failed always fire for scheduled sends, fire for immediate Send Message requests only when you turn on synchronous send webhooks in the Dashboard (Customizations > Notifications), and never fire for Send Draft.Diagram as text
- Your app: Your app has a message ready: It goes out from a grant (a connected mailbox). Choose a path by when the message should go out and whether a person reviews it first.
- One of these paths:
- Send now (recommended by default)
- Your app: Send the message right away: One request. Your app gets the answer straight back: the email provider accepted the message or rejected it. Request:
POST /.v3/ grants/ {grant_id}/ messages/ send
- Your app: Send the message right away: One request. Your app gets the answer straight back: the email provider accepted the message or rejected it. Request:
- Draft first (recommended when a person reviews)
- Your app: Save a draft for review: Nylas saves the message in the user's Drafts folder, where a person can check it. The draft doesn't need recipients (
to) yet, and anymetadatacarries over to the sent message. Request:POST /.v3/ grants/ {grant_id}/ drafts - Your app: Update the draft: Optional. Change the draft as often as needed. Request:
PUT /.v3/ grants/ {grant_id}/ drafts/ {draft_id} - Your app: Send the approved draft: The draft goes out right away. A saved draft can't be scheduled for later (no
send_at). Request:POST /.v3/ grants/ {grant_id}/ drafts/ {draft_id}
- Your app: Save a draft for review: Nylas saves the message in the user's Drafts folder, where a person can check it. The draft doesn't need recipients (
- Schedule for later
- Your app: Schedule the message: Your app sends the message with a future send time (
send_at). Nylas returns aschedule_idyou can use to cancel it. Request:POST /withv3/ grants/ {grant_id}/ messages/ send send_at.- Where it waits: Nylas holds it (default).
send_atmust be 1 minute to 30 days ahead. - Where it waits: The user's Drafts folder holds it when you set
use_draft: true, with no maximum. Google and Microsoft only. This is what scheduling a draft means: you can't schedule a draft you already created.
- Where it waits: Nylas holds it (default).
- Nylas: Hold it until the send time: You can still cancel up to 10 seconds before the send time. Request:
DELETE /.v3/ grants/ {grant_id}/ messages/ schedules/ {scheduleId}
- Your app: Schedule the message: Your app sends the message with a future send time (
- Send now (recommended by default)
- Provider: The mailbox sends the message: The user's email provider (or Nylas, for an Agent Account) sends the message from the grant's own address, so recipients see that address as the sender.
- Ends in one of:
- Accepted → Sent: The message appears in the mailbox's Sent folder. Nylas notifies your app with
message.send_successfor scheduled sends, and for Send Message when synchronous send webhooks are on. - Rejected → Failed: For immediate sends, the API response contains the error. Nylas notifies your app with
message.send_failedfor scheduled sends, and for Send Message when synchronous send webhooks are on.
- Accepted → Sent: The message appears in the mailbox's Sent folder. Nylas notifies your app with
- Send a message directly: compose and send in a single request. Use this when your app writes the message and no one needs to review it.
- Create and send a draft: prepare a message, save it as a draft synced with the provider, and send it when ready. Use this when a person reviews or edits the message before it goes out.
- Schedule a message: add
send_atto a Send Message request. See the scheduled send lifecycle for how Nylas holds, cancels, and reports on scheduled messages.
Sending a message directly and sending a draft are both synchronous. The request blocks until the provider accepts or rejects the message. If the provider throttles the send, Nylas returns the 429 right away without retrying, for every provider. When the provider says how long to wait, the response includes a Retry-After header with that number of seconds. Your application decides whether and when to retry. Agent Accounts are the exception: a send over the per-second rate limit can be queued and return 202 Accepted instead of 429. See Send queueing.
When you make a Send Message request, Nylas connects to the provider and sends the message as the user. Providers see this as the user sending directly, not as an external platform acting on their behalf. This gives you high deliverability, but messages are subject to the provider’s rate limits and abuse detection. See Improve email deliverability for best practices.
Create and send a draft
Section titled “Create and send a draft”To prepare a message you don’t need to send immediately, create a draft and send it later. Nylas syncs the draft with the provider’s Drafts folder. Metadata you set on the draft carries over to the sent message.
Make a Create Draft request with the message content:
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/drafts' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "subject": "With Love, From Nylas", "to": [{ "email": "[email protected]", "name": "Leyah Miller" }], "cc": [{ "email": "[email protected]", "name": "Nyla" }], "bcc": [{ "email": "[email protected]", "name": "Nylas DevRel" }], "reply_to": [{ "email": "[email protected]", "name": "Nylas" }], "body": "This email was sent using the Nylas Email API. Visit https://nylas.com for details.", "tracking_options": { "opens": true, "links": true, "thread_replies": true, "label": "Just testing" } }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});const identifier = "<NYLAS_GRANT_ID>";
const createDraft = async () => { try { const draft = { subject: "Your Subject Here", body: "Your email body here.", };
const createdDraft = await nylas.drafts.create({ identifier, requestBody: draft, });
console.log("Draft created:", createdDraft); } catch (error) { console.error("Error creating draft:", error); }};
createDraft();from nylas import Clientfrom nylas import utils
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"email = "<EMAIL>"
attachment = utils.file_utils.attach_file_request_builder("Nylas_Logo.png")
draft = nylas.drafts.create( grant_id, request_body={ "to": [{ "name": "Name", "email": email }], "reply_to": [{ "name": "Name", "email": email }], "subject": "Your Subject Here", "body": "Your email body here.", "attachments": [attachment] })
print(draft)require 'nylas'
# Initialize Nylas clientnylas = Nylas::Client.new( api_key: "<NYLAS_API_KEY>")
file = Nylas::FileUtils.attach_file_request_builder("Nylas_Logo.png")
request_body = { subject: "From Nylas", body: 'This email was sent using the ' + 'Nylas email API. ' + 'Visit https://nylas.com for details.', to: [{ name: "Dorothy Vaughan", cc: [{ name: "George Washington Carver", bcc: [{ name: "Albert Einstein", reply_to: [{ name: "Stephanie Kwolek", tracking_options: {label: "hey just testing", opens: true, links: true, thread_replies: true}, attachments: [file]}
draft, _ = nylas.drafts.create(identifier: "<NYLAS_GRANT_ID>", request_body: request_body)puts "Draft \"#{draft[:subject]}\" was created with ID: #{draft[:id]}"import com.nylas.NylasClient;import com.nylas.models.*;import com.nylas.util.FileUtils;import java.util.ArrayList;import java.util.Collections;import java.util.List;
public class CreateDraft { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
CreateAttachmentRequest attachment = FileUtils.attachFileRequestBuilder("src/main/java/Nylas_Logo.png"); List<CreateAttachmentRequest> request = new ArrayList<>(); request.add(attachment);
CreateDraftRequest requestBody = new CreateDraftRequest.Builder(). subject("With Love, from Nylas"). body("This email was sent using the Nylas email API. Visit https://nylas.com for details."). attachments(request). build();
Response<Draft> drafts = nylas.drafts().create("<NYLAS_GRANT_ID>", requestBody);
System.out.println("Draft " + drafts.getData().getSubject() + " was created with ID " + drafts.getData().getId()); }}import com.nylas.NylasClientimport com.nylas.models.*import com.nylas.util.FileUtils
fun main(args: Array<String>) { val nylas: NylasClient = NylasClient( apiKey = "<NYLAS_API_KEY>" )
val attachment: CreateAttachmentRequest = FileUtils.attachFileRequestBuilder("src/main/kotlin/Nylas_Logo.png")
val requestBody = CreateDraftRequest( subject = "With Love, from Nylas", body = "This email was sent using the Nylas Email API. Visit https://nylas.com for details.", attachments = listOf(attachment) )
val draft = nylas.drafts().create("<NYLAS_GRANT_ID>", requestBody).data
print("Draft " + draft.subject + " was created with ID: " + draft.id)}The Create Draft request body accepts the same fields as the Send Message request, with these differences:
tois optional for drafts (you can save an incomplete draft).starred(boolean) is supported for drafts but not direct sends.send_atanduse_draftare not available on Create Draft. Use Send Message withsend_atfor scheduled sends.
When you’re ready, send the draft by making a Send Draft request:
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/drafts/<DRAFT_ID>' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});const identifier = "<NYLAS_GRANT_ID>";const draftId = "<DRAFT_ID>";
const sendDraft = async () => { try { const sentMessage = await nylas.drafts.send({ identifier, draftId }); console.log("Draft sent:", sentMessage); } catch (error) { console.error("Error sending draft:", error); }};
sendDraft();from nylas import Client
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"draft_id = "<DRAFT_ID>"
draft = nylas.drafts.send( grant_id, draft_id)
print(draft)require 'nylas'
nylas = Nylas::Client.new( api_key: "<NYLAS_API_KEY>")
draft, _ = nylas.drafts.send(identifier: "<NYLAS_GRANT_ID>", draft_id: "<DRAFT_ID>")import com.nylas.NylasClient
fun main(args: Array<String>) {
val nylas: NylasClient = NylasClient( apiKey = "<NYLAS_API_KEY>" )
val draft = nylas.drafts().send("<NYLAS_GRANT_ID>", "<DRAFT_ID>") print(draft.data)}import com.nylas.NylasClient;import com.nylas.models.*;
public class SendDraft { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
Response<Message> draft = nylas.drafts().send("<NYLAS_GRANT_ID>", "<DRAFT_ID>"); System.out.println(draft.getData()); }}# Step 1: Create the draftnylas email drafts create \ --subject "Review this draft" \ --body "Please review and let me know."
# Step 2: Send it by ID when readynylas email drafts send <DRAFT_ID>Send a message directly
Section titled “Send a message directly”To send a message without creating a draft, make a Send Message 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 '{ "subject": "Reaching Out with Nylas", "body": "Reaching out using the <a href='https://www.nylas.com/products/email-api/'>Nylas Email API</a>", "to": [{ "name": "Leyah Miller", "email": "[email protected]" }], "tracking_options": { "opens": true, "links": true, "thread_replies": true, "label": "hey just testing" } }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
async function sendEmail() { try { const sentMessage = await nylas.messages.send({ identifier: "<NYLAS_GRANT_ID>", requestBody: { to: [{ name: "Name", email: "<EMAIL>" }], replyTo: [{ name: "Name", email: "<EMAIL>" }], replyToMessageId: "<MESSAGE_ID>", subject: "Your Subject Here", body: "Your email body here.", }, });
console.log("Sent message:", sentMessage); } catch (error) { console.error("Error sending email:", error); }}
sendEmail();import osimport sysfrom nylas import Clientfrom nylas import utils
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"email = "<EMAIL>"
attachment = utils.file_utils.attach_file_request_builder("Nylas_Logo.png")
message = nylas.messages.send( grant_id, request_body={ "to": [{ "name": "Name", "email": email }], "reply_to": [{ "name": "Name", "email": email }], "reply_to_message_id": "<MESSAGE_ID>", "subject": "Your Subject Here", "body": "Your email body here.", "attachments": [attachment] })
print(message)require 'nylas'
# Initialize the Nylas clientnylas = Nylas::Client.new( api_key: '<NYLAS_API_KEY>', api_uri: '<NYLAS_API_URI>')
grant_id = '<NYLAS_GRANT_ID>'email = '<EMAIL>'
# Prepare the attachmentattachment = Nylas::FileUtils.attach_file_request_builder('Nylas_Logo.png')
# Send the messagemessage, _request_id = nylas.messages.send( identifier: grant_id, request_body: { to: [{ name: 'Name', email: email }], reply_to: [{ name: 'Name', email: email }], reply_to_message_id: '<MESSAGE_ID>', subject: 'Your Subject Here', body: 'Your email body here.', attachments: [attachment] })
puts messageimport com.nylas.NylasClientimport com.nylas.models.*import com.nylas.util.FileUtils
fun main(args: Array<String>) {
val nylas: NylasClient = NylasClient( apiKey = "<NYLAS_API_KEY>" )
val attachment: CreateAttachmentRequest = FileUtils.attachFileRequestBuilder("src/main/kotlin/Nylas_Logo.png")
val options = TrackingOptions("hey just testing", true, true, true)
"John Doe")) val requestBody : SendMessageRequest = SendMessageRequest.Builder(emailNames). subject("Hey Reaching Out with Nylas"). body("Hey I would like to track this link <a href='https://espn.com'>My Example Link</a>"). trackingOptions(options). attachments(listOf(attachment)). build() val email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody) print(email.data)}import com.nylas.NylasClient;import com.nylas.models.*;import java.util.ArrayList;import java.util.List;import com.nylas.util.FileUtils;
public class SendEmails { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
CreateAttachmentRequest attachment = FileUtils.attachFileRequestBuilder("src/main/java/Nylas_Logo.png"); List<CreateAttachmentRequest> request = new ArrayList<>(); request.add(attachment);
List<EmailName> emailNames = new ArrayList<>(); "John Doe"));
TrackingOptions options = new TrackingOptions("hey just testing", true, true, true);
SendMessageRequest requestBody = new SendMessageRequest.Builder(emailNames). trackingOptions(options). subject("Hey Reaching Out with Nylas"). body("Hey I would like to track this link <a href='https://espn.com'>My Example Link</a>."). attachments(request). build();
Response<Message> email = nylas.messages().send("<NYLAS_GRANT_ID>", requestBody); System.out.println(email.getData()); }}nylas email send \ --subject "Project Update" \ --body "Here's the latest status on the project..."Send Message request fields
Section titled “Send Message request fields”| Field | Type | Required | Description |
|---|---|---|---|
to | array[object] | Yes | Recipients. Each object includes name (string) and email (string, required). |
subject | string | No | Email subject line. |
body | string | No | Message body. HTML is supported unless is_plaintext is true. |
is_plaintext | boolean | No | When true, body is sent as plain text instead of HTML. Default: false. |
cc | array[object] | No | CC recipients. Same format as to. |
bcc | array[object] | No | BCC recipients. Same format as to. |
from | array[object] | No | Sender override. See Override sender display name. |
reply_to | array[object] | No | Reply-to addresses. Same format as to. |
reply_to_message_id | string | No | ID of the message being replied to. Nylas adds In-Reply-To and References headers automatically. |
attachments | array[object] | No | File attachments. Each object includes filename, content (Base64-encoded), and content_type. See Attachments. For files larger than 25MB on Microsoft grants, use the large attachments flow and pass { "id": "<attachment_id>" }. |
custom_headers | array[object] | No | Custom email headers. Each object includes name and value. See Headers and MIME data. |
tracking_options | object | No | Open, link click, and thread reply tracking. See Tracking messages. |
send_at | integer | No | Unix timestamp for scheduled delivery. Must be at least 1 minute in the future, max 30 days. See Scheduling messages. |
use_draft | boolean | No | Google and Microsoft only. When true with send_at, saves the message in the Drafts folder until send time. Default: false. |
template | object | No | Template to merge into the message. Includes id (required), strict, and variables. See Templates and workflows. |
metadata | object | No | String key-value pairs. Max 50 pairs, keys max 40 characters, values max 500. See Using metadata. |
For the full schema including nested object details, see the Send Message API reference.
Override sender display name
Section titled “Override sender display name”You can set the sender’s display name by including the from.name parameter in your 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 '{ "subject": "Reaching out with Nylas", "to": [{ "name": "Leyah Miller", "email": "[email protected]" }], "from": [{ "name": "Nyla", "email": "[email protected]" }] }'If you don’t include the from field, Nylas uses the account’s default display name.
Attach a signature
Section titled “Attach a signature”You can automatically append an HTML signature to any outgoing message by including the signature_id field in your request body. Nylas appends the signature after the message body at send time, including after quoted text in replies and forwards.
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 '{ "subject": "Quarterly update", "to": [{ "name": "Leyah Miller", "email": "[email protected]" }], "body": "<p>Hi Leyah, here is the quarterly update...</p>", "signature_id": "<SIGNATURE_ID>" }'The signature_id field also works on Create Draft and Send Draft requests. See Using email signatures for the full guide on creating, managing, and integrating signatures.
Keep in mind
Section titled “Keep in mind”- Send operations are synchronous. The request blocks until the provider accepts or rejects the message. Nylas returns a throttled send as a
429without retrying it, so your application owns retries. Wait the number of seconds inRetry-Afterwhen the response includes it. On Agent Accounts, a send over the per-second rate limit can be queued and return202instead. - Use idempotency keys for safe retries. Pass an
Idempotency-Keyheader to retry without sending duplicate emails. See Idempotent send requests. - Provider rate limits apply. Each provider limits how many messages you can send per day. See Provider rate limits for details. For high-volume sending, consider a transactional service like SendGrid, Mailgun, or Amazon SES.
- Microsoft and iCloud don’t support
List-Unsubscribe-PostorList-Unsubscribeheaders. Nylas can’t add these headers for messages sent from Microsoft Graph or iCloud accounts. - Some providers move messages between folders after sending (for example, from Outbox to Sent), which may trigger multiple
message.updatednotifications for a single send. - Nylas de-duplicates some webhook notifications, so you may receive a
message.updatednotification instead of amessage.creatednotification after sending. - Bounced messages are detected automatically. See Bounce detection below.
Synchronous send webhooks
Section titled “Synchronous send webhooks”Subscribe to the message.send_success and message.send_failed notification triggers to track the outcome of a send.
By default, these webhooks fire only for scheduled sends (sends that use send_at). To also receive them for synchronous (immediate) sends, enable Synchronous send webhooks in the Nylas Dashboard: open your application, go to Customizations → Notifications, and turn on the setting.
Bounce detection
Section titled “Bounce detection”Nylas monitors for and notifies you when a user gets a message bounce notification. A bounce notification is a message sent by a recipient’s provider when a message can’t be delivered. It usually includes a detailed error message explaining the cause.
When a user receives a bounce notification, Nylas scans it and extracts the reason from the error message.
Messages can bounce for temporary or permanent reasons:
- Soft bounces have a temporary root cause, like the recipient’s inbox being full.
- Hard bounces have a permanent root cause, like the recipient’s email address no longer existing.
Currently, Nylas detects hard bounces only and publishes mailbox_unavailable and domain_not_found bounce types.
For more information, see the message.bounce_detected notification schema and Soft vs. Hard Bounce on the Nylas blog.
What’s next
Section titled “What’s next”- Idempotent send requests to retry safely without sending duplicates
- Sending errors for error codes and troubleshooting
- Improve email deliverability for best practices
- Send email from the terminal — test email sending across providers without writing code