Exchange on-premises servers are still common in enterprise environments. If your users run self-hosted Exchange (2007 or later), Nylas connects to them through Exchange Web Services (EWS), a separate protocol from the Microsoft Graph API used for Exchange Online and Microsoft 365.
The same Messages API you use for Gmail and Outlook works for Exchange on-prem accounts. This guide covers the EWS-specific details: when to use EWS vs. Microsoft Graph, authentication, message ID behavior, and search capabilities.
EWS vs. Microsoft Graph: which one?
Section titled “EWS vs. Microsoft Graph: which one?”This is the first thing to figure out. The two provider types target different Exchange deployments:
| Provider type | Connector | Use when |
|---|---|---|
| Microsoft Graph | microsoft | Exchange Online, Microsoft 365, Office 365, Outlook.com, personal Microsoft accounts |
| Exchange Web Services | ews | Self-hosted Exchange servers (on-premises) |
If the user’s mailbox is hosted by Microsoft in the cloud, use the Microsoft guide instead. The ews connector is specifically for organizations that run their own Exchange servers.
Microsoft announced EWS retirement and recommends migrating to Microsoft Graph. However, many organizations still run on-premises Exchange servers where EWS is the only option. Nylas continues to support EWS for these environments.
Why use Nylas instead of EWS directly?
Section titled “Why use Nylas instead of EWS directly?”EWS is a SOAP-based XML API. Every request requires building XML SOAP envelopes, every response needs XML parsing, and errors come back as SOAP faults with nested XML structures. You need to handle autodiscovery to find the right server endpoint (which is frequently misconfigured in enterprise environments), manage authentication with support for two-factor app passwords, and work with EWS-specific data formats that don’t match any other email provider.
Nylas replaces all of that with a JSON REST API. No XML, no WSDL, no SOAP. Authentication and autodiscovery are handled automatically. Your code stays the same whether you’re reading from Exchange on-prem, Exchange Online, Gmail, or any IMAP provider.
If you have deep EWS experience and only target Exchange on-prem, you can integrate directly. For multi-provider support or faster time-to-integration, Nylas is the simpler path.
Before you begin
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for an Exchange on-premises account
- An EWS connector configured with the appropriate scopes
- The Exchange server accessible from outside the corporate network (not behind a VPN or firewall that blocks external access)
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
Exchange authentication setup
Section titled “Exchange authentication setup”Create an EWS connector with the scopes your app needs:
| Scope | Access |
|---|---|
ews.messages | Email API (messages, drafts, folders) |
ews.calendars | Calendar API |
ews.contacts | Contacts API |
During authentication, users sign in with their Exchange credentials, typically the same username and password they use for Windows login. The username format is usually [email protected] or DOMAIN\username.
If EWS autodiscovery is configured on the server, authentication works automatically. If not, users can click “Additional settings” and manually enter the Exchange server address (e.g., mail.company.com).
Users with two-factor authentication must generate an app password instead of using their regular password. See Microsoft’s app password documentation for instructions.
The full setup walkthrough is in the Exchange on-premises provider guide.
Network requirements
Section titled “Network requirements”The Exchange server must be accessible from Nylas’s infrastructure:
- EWS must be enabled on the server and exposed outside the corporate network
- If the server is behind a firewall, you’ll need to allow Nylas’s IP addresses (available on contract plans with static IPs)
- If EWS isn’t enabled, Nylas can fall back to IMAP for email-only access, but you lose calendar and contacts support
Accounts in admin groups are not supported.
List messages
Section titled “List messages”Make a List Messages request with the grant ID. By default, Nylas returns the 50 most recent messages. These examples limit results to 5:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=5" \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json'{ "request_id": "d0c951b9-61db-4daa-ab19-cd44afeeabac", "data": [ { "starred": false, "unread": true, "folders": [ "UNREAD", "CATEGORY_PERSONAL", "INBOX" ], "grant_id": "1", "date": 1706811644, "attachments": [ { "id": "1", "grant_id": "1", "filename": "invite.ics", "size": 2504, "content_type": "text/calendar; charset=\"UTF-8\"; method=REQUEST" }, { "id": "2", "grant_id": "1", "filename": "invite.ics", "size": 2504, "content_type": "application/ics; name=\"invite.ics\"", "is_inline": false, "content_disposition": "attachment; filename=\"invite.ics\"" } ], "from": [ { "name": "Nylas DevRel", } ], "id": "1", "object": "message", "snippet": "Send Email with Nylas APIs", "subject": "Learn how to Send Email with Nylas APIs", "thread_id": "1", "to": [ { "name": "Nyla", } ], "created_at": 1706811644, "body": "Learn how to send emails using the Nylas APIs!" } ], "next_cursor": "123"}app.get("/nylas/recent-emails", async (req, res) => { try { const identifier = process.env.USER_GRANT_ID; const messages = await nylas.messages.list({ identifier, queryParams: { limit: 5, }, });
res.json(messages); } catch (error) { console.error("Error fetching emails:", error); }});from dotenv import load_dotenvload_dotenv()
import osimport sysfrom nylas import Client
nylas = Client( os.environ.get('NYLAS_API_KEY'), os.environ.get('NYLAS_API_URI'))
grant_id = os.environ.get("NYLAS_GRANT_ID")
messages = nylas.messages.list( grant_id, query_params={ "limit": 5 })
print(messages)require 'nylas'
nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')query_params = { limit: 5 }messages, _ = nylas.messages.list(identifier: '<NYLAS_GRANT_ID>', query_params: query_params)
messages.each {|message| puts "[#{Time.at(message[:date]).strftime("%d/%m/%Y at %H:%M:%S")}] \ #{message[:subject]}"}import com.nylas.NylasClient;import com.nylas.models.*;import java.text.SimpleDateFormat;
public class ListMessages { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build(); ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder().limit(5).build(); ListResponse<Message> message = nylas.messages().list("<NYLAS_GRANT_ID>", queryParams);
for(Message email : message.getData()) { String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(new java.util.Date((email.getDate() * 1000L)));
System.out.println("[" + date + "] | " + email.getSubject()); } }}import com.nylas.NylasClientimport com.nylas.models.*import java.text.SimpleDateFormatimport java.util.*
fun main(args: Array<String>) { val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>") val queryParams = ListMessagesQueryParams(limit = 5) val messages = nylas.messages().list("<NYLAS_GRANT_ID>", queryParams).data
for (message in messages) { val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(Date(message.date.toLong() * 1000)) println("[$date] | ${message.subject}") }}Filter messages
Section titled “Filter messages”You can narrow results with query parameters. Here’s what works with Exchange accounts:
| Parameter | What it does | Example |
|---|---|---|
subject | Match on subject line | ?subject=Weekly standup |
from | Filter by sender | [email protected] |
to | Filter by recipient | [email protected] |
unread | Unread messages only | ?unread=true |
in | Filter by folder or label ID | ?in=INBOX |
received_after | After a Unix timestamp | ?received_after=1706000000 |
received_before | Before a Unix timestamp | ?received_before=1706100000 |
has_attachment | Only messages with attachments | ?has_attachment=true |
Here’s how to combine filters. This pulls unread messages from a specific sender:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/[email protected]&unread=true&limit=10" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const messages = await nylas.messages.list({ identifier: grantId, queryParams: { unread: true, limit: 10, },});messages = nylas.messages.list( grant_id, query_params={ "unread": True, "limit": 10, })Search with search_query_native
Section titled “Search with search_query_native”Exchange supports the search_query_native parameter using Microsoft’s Advanced Query Syntax (AQS). You can combine search_query_native with any query parameter except thread_id.
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?search_query_native=subject%3Ainvoice&limit=10" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const messages = await nylas.messages.list({ identifier: grantId, queryParams: { searchQueryNative: "subject:invoice", limit: 10, },});messages = nylas.messages.list( grant_id, query_params={ "search_query_native": "subject:invoice", "limit": 10, })AQS queries must be URL-encoded. For example, subject:invoice becomes subject%3Ainvoice in the URL. The SDKs handle this automatically, but you’ll need to encode manually in curl requests.
Exchange doesn’t support searching by BCC field. If you include BCC in a search_query_native query, results may be incomplete or return an error.
The Exchange server must have the AQS parser enabled and search indexing active for search_query_native to work. If queries aren’t returning expected results, the Exchange admin should verify these settings.
See the search best practices guide for more on search_query_native across providers.
Things to know about Exchange
Section titled “Things to know about Exchange”Exchange on-prem behaves differently from Exchange Online (Microsoft Graph) in several important ways.
Message IDs change when messages move
Section titled “Message IDs change when messages move”This is the most important Exchange-specific behavior. When a message is moved from one folder to another, its Nylas message ID changes. This is expected EWS behavior because Exchange assigns new IDs when messages change folders.
If your app stores message IDs, treat them as folder-specific pointers, not permanent identifiers. For tracking a message across folder moves, use the InternetMessageId header instead. It stays stable regardless of which folder the message is in.
To get the InternetMessageId, include the fields=include_headers query parameter:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>?fields=include_headers" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const message = await nylas.messages.find({ identifier: grantId, messageId: messageId, queryParams: { fields: "include_headers", },});message = nylas.messages.find( grant_id, message_id, query_params={ "fields": "include_headers", })Multiple messages can share the same InternetMessageId in Exchange. This happens when copies of a message exist in multiple folders. Use it for correlation, not as a unique key.
Folder hierarchy with parent_id
Section titled “Folder hierarchy with parent_id”Exchange supports nested folders. Nylas returns a flat folder list but includes a parent_id field on child folders so you can reconstruct the hierarchy. Use parent_id when creating or updating folders to place them in the right location.
Use the List Folders endpoint to discover all folders and their hierarchy for an Exchange account.
Starred messages require Exchange 2010+
Section titled “Starred messages require Exchange 2010+”The starred query parameter only works on Exchange 2010 and later. If you’re targeting Exchange 2007, filtering by starred status isn’t available.
Rate limits are admin-configured
Section titled “Rate limits are admin-configured”Unlike Google and Microsoft’s cloud services, Exchange on-prem rate limits are set by the server administrator. Nylas can’t predict what they’ll be. If the Exchange server throttles a request, Nylas returns a Retry-After header with the number of seconds to wait.
For apps that check mailboxes frequently, webhooks are the best way to avoid hitting rate limits. Let Nylas notify you of changes instead of polling.
Date filtering is day-level precision
Section titled “Date filtering is day-level precision”Exchange processes received_before and received_after filters at the day level, not second-level. Even though Nylas accepts Unix timestamps, Exchange rounds to the nearest day. Results are inclusive of the specified day.
For example, if you filter with received_after=1706745600 (February 1, 2024 00:00:00 UTC), you’ll get all messages from February 1 onward, including messages received earlier that day.
Search indexing affects query accuracy
Section titled “Search indexing affects query accuracy”Exchange relies on a search index for queries using to, from, cc, bcc, and any_email. If a message has just arrived but the search index hasn’t refreshed yet, it won’t appear in filtered results. The search index refresh interval is controlled by the Exchange administrator.
If filtered queries aren’t returning recently received messages, this is likely the cause. Unfiltered list requests (no query parameters) always return the latest messages.
EWS fallback to IMAP
Section titled “EWS fallback to IMAP”If EWS isn’t enabled on the Exchange server, Nylas can still connect via IMAP for email-only access. This means:
- Email works with messages, drafts, and folders available
- Calendar and contacts are not available because these require EWS
- The 90-day message cache applies because IMAP connections use the same caching behavior as other IMAP providers, with
query_imap=truefor older messages
If your users report that calendar or contacts aren’t working, verify that EWS is enabled on their Exchange server.
Paginate through results
Section titled “Paginate through results”The Messages API returns paginated responses. When there are more results, the response includes a next_cursor value. Pass it back as page_token to get the next page:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=10&page_token=<NEXT_CURSOR>" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'let pageCursor = undefined;
do { const result = await nylas.messages.list({ identifier: grantId, queryParams: { limit: 10, pageToken: pageCursor, }, });
// Process result.data here
pageCursor = result.nextCursor;} while (pageCursor);page_cursor = None
while True: query = {"limit": 10} if page_cursor: query["page_token"] = page_cursor
result = nylas.messages.list(grant_id, query_params=query)
# Process result.data here
page_cursor = result.next_cursor if not page_cursor: breakKeep paginating until the response comes back without a next_cursor.
What’s next
Section titled “What’s next”- Messages API reference for full endpoint documentation and all available parameters
- Using the Messages API for search, modification, and deletion
- Threads to group related messages into conversations
- Search best practices for advanced search with
search_query_nativeand AQS for Exchange - Webhooks for real-time notifications instead of polling
- Exchange on-premises provider guide for full Exchange setup including authentication and network requirements
- Microsoft admin approval to configure consent for enterprise organizations (Exchange Online)