Exchange on-premises servers are still common in enterprise environments, especially in regulated industries and government organizations. If your users run self-hosted Exchange (2007 or later), Nylas connects to their calendars through Exchange Web Services (EWS) - a separate protocol from the Microsoft Graph API used for Exchange Online and Microsoft 365.
The same Events API you use for Google Calendar and Outlook works for Exchange on-prem accounts. This guide covers the EWS-specific details: when to use EWS vs. Microsoft Graph, authentication and autodiscovery, recurring event restrictions, and on-prem networking considerations.
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 |
| Exchange Web Services | ews | Self-hosted Exchange servers (on-premises) |
If the user’s calendar 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. Calendar operations are particularly verbose in EWS - creating a recurring event with attendees, timezone rules, and reminders means constructing deeply nested XML payloads. You also need to handle autodiscovery to find the right server endpoint (which is frequently misconfigured), manage credential-based authentication with support for two-factor app passwords, and deal with Exchange’s recurrence model that doesn’t map cleanly to iCalendar standards.
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 calendar events from Exchange on-prem, Exchange Online, Google Calendar, or iCloud.
If you have deep EWS experience and only target Exchange on-prem, direct integration is an option. For multi-provider calendar 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
ews.calendarsscope - 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.
Autodiscovery and authentication
Section titled “Autodiscovery and authentication”EWS uses credential-based authentication. During the auth flow, 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, Nylas automatically locates the correct EWS endpoint. If autodiscovery is disabled or misconfigured, users can click “Additional settings” during authentication and manually enter the Exchange server address (for example, 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.
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 |
The full setup walkthrough is in the Exchange on-premises provider guide.
List events
Section titled “List events”Make a List Events request with the grant ID and a calendar_id. Nylas returns the most recent events by default. You can use primary as the calendar_id to target the account’s default calendar. These examples limit results to 5:
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>&start=<TIMESTAMP>&end=<TIMESTAMP>' \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json'{ "request_id": "cbd60372-df33-41d3-b203-169ad5e3AAAA", "data": [{ "busy": true, "calendar_id": "primary", "conferencing": { "details": { "meeting_code": "ist-****-tcz", "url": "https://meet.google.com/ist-****-tcz" }, "provider": "Google Meet" }, "created_at": 1701974804, "creator": { "name": "" }, "description": null, "grant_id": "1e3288f6-124e-405d-a13a-635a2ee54eb2", "hide_participants": false, "html_link": "https://www.google.com/calendar/event?eid=NmE0dXIwabQAAAA", "id": "6aaaaaaame8kpgcid6hvd", "object": "event", "organizer": { "name": "" }, "participants": [ { "status": "yes" }, { "status": "yes" } ], "read_only": true, "reminders": { "overrides": null, "use_default": true }, "status": "confirmed", "title": "Holiday check in", "updated_at": 1701974915, "when": { "end_time": 1701978300, "end_timezone": "America/Los_Angeles", "object": "timespan", "start_time": 1701977400, "start_timezone": "America/Los_Angeles" } }]}import 'dotenv/config'import Nylas from 'nylas'
const NylasConfig = { apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.NYLAS_API_URI}
const nylas = new Nylas(NylasConfig)
async function fetchAllEventsFromCalendar() { try { const events = await nylas.events.list({ identifier: process.env.NYLAS_GRANT_ID, queryParams: { calendarId: process.env.CALENDAR_ID, } })
console.log('Events:', events) } catch (error) { console.error('Error fetching calendars:', error) }}
fetchAllEventsFromCalendar()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")
events = nylas.events.list( grant_id, query_params={ "calendar_id": os.environ.get("CALENDAR_ID") })
print(events)require 'nylas'
nylas = Nylas::Client.new(api_key: "<NYLAS_API_KEY>")
query_params = { calendar_id: "<CALENDAR_ID>"}
# Read events from our main calendar in the specified date and timeevents, _request_ids = nylas.events.list(identifier: "<NYLAS_GRANT_ID>", query_params: query_params)
events.each {|event| case event[:when][:object] when 'timespan' start_time = Time.at(event[:when][:start_time]).strftime("%d/%m/%Y at %H:%M:%S") end_time = Time.at(event[:when][:end_time]).strftime("%d/%m/%Y at %H:%M:%S") event_date = "The time of the event is from: #{start_time} to #{end_time}" when 'datespan' start_time = event[:when][:start_date] end_time = event[:when][:end_date] event_date = "The date of the event is from: #{start_time} to: #{end_time}" when 'date' start_time = event[:when][:date] event_date = "The date of the event is: #{start_time}" end event[:participants].each {|participant| participant_details += "Email: #{participant[:email]} " \ "Name: #{participant[:name]} Status: #{participant[:status]} - " } print "Id: #{event[:id]} | Title: #{event[:title]} | #{event_date} | " puts "Participants: #{participant_details.chomp(' - ')}" puts "\n"}import com.nylas.NylasClient;import com.nylas.models.When;
import com.nylas.models.*;import java.text.SimpleDateFormat;import java.util.List;import java.util.Objects;
public class read_calendar_events { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
// Build the query parameters to filter our the results ListEventQueryParams listEventQueryParams = new ListEventQueryParams.Builder("<CALENDAR_ID>").build();
// Read the events from our main calendar List<Event> events = nylas.events().list("<NYLAS_GRANT_ID>", listEventQueryParams).getData();
for (Event event : events) { System.out.print("Id: " + event.getId() + " | "); System.out.print("Title: " + event.getTitle());
// Dates are handled differently depending on the event type switch (Objects.requireNonNull(event.getWhen().getObject()).getValue()) { case "datespan" -> { When.Datespan date = (When.Datespan) event.getWhen();
System.out.print(" | The date of the event is from: " + date.getStartDate() + " to " + date.getEndDate()); } case "date" -> { When.Date date = (When.Date) event.getWhen();
System.out.print(" | The date of the event is: " +date.getDate()); } case "timespan" -> { When.Timespan timespan = (When.Timespan) event.getWhen();
String initDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(new java.util.Date((timespan.getStartTime() * 1000L)));
String endDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(new java.util.Date((timespan.getEndTime() * 1000L)));
System.out.print(" | The time of the event is from: " + initDate + " to " + endDate); } }
System.out.print(" | Participants: ");
for(Participant participant : event.getParticipants()){ System.out.print(" Email: " + participant.getEmail() + " Name: " + participant.getName() + " Status: " + participant.getStatus()); }
System.out.println("\n"); } }}import com.nylas.NylasClientimport com.nylas.models.*
import java.util.*
fun main(args: Array<String>) { val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
val eventquery: ListEventQueryParams = ListEventQueryParams(calendarId = "<CALENDAR_ID>")
// Get a list of events val myevents: List<Event> = nylas.events().list( "<NYLAS_GRANT_ID>", queryParams = eventquery).data
// Loop through the events for(event in myevents){ print("Id: " + event.id + " | "); print("Title: " + event.title);
// Get the details of Date and Time of each event. when(event.getWhen().getObject().toString()) { "DATE" -> { val datespan = event.getWhen() as When.Date
print(" | The date of the event is: " + datespan.date); } "DATESPAN" -> { val datespan = event.getWhen() as When.Datespan
print(" | The date of the event is: " + datespan.startDate); } "TIMESPAN" -> { val timespan = event.getWhen() as When.Timespan val startDate = Date(timespan.startTime.toLong() * 1000) val endDate = Date(timespan.endTime.toLong() * 1000)
print(" | The time of the event is from: $startDate to $endDate"); } }
print(" | Participants: ");
// Get a list of the event participants val participants = event.participants
// Loop through and print their email, name and status for(participant in participants) { print(" Email: " + participant.name + " Name: " + participant.email + " Status: " + participant.status) }
println("\n") }}The calendar_id=primary shortcut works for EWS accounts, targeting the user’s default calendar. The response format is identical across providers, so your parsing logic works the same for Exchange on-prem, Exchange Online, Google, and iCloud.
Filter events
Section titled “Filter events”You can narrow results with query parameters. Here’s what works with Exchange accounts:
| Parameter | What it does | Example |
|---|---|---|
calendar_id | Required. Filter by calendar | ?calendar_id=primary |
title | Match on event title (case insensitive) | ?title=standup |
description | Match on description (case insensitive) | ?description=quarterly |
location | Match on location (case insensitive) | ?location=Room%20A |
start | Events starting at or after a Unix timestamp | ?start=1706000000 |
end | Events ending at or before a Unix timestamp | ?end=1706100000 |
attendees | Filter by attendee email (comma-delimited) | [email protected] |
busy | Filter by busy status | ?busy=true |
metadata_pair | Filter by metadata key-value pair | ?metadata_pair=project_id:abc123 |
Exchange (EWS) supports several additional filter parameters:
tentative_as_busy- treat tentative events as busy when checking availability (defaults totrue)updated_after/updated_before- filter by last-modified timestamp, useful for incremental syncical_uid- find a specific event by its iCalendar UIDmaster_event_id- list all instances and overrides for a specific recurring series
A few parameters are not supported on EWS:
show_cancelled- Exchange does not support retrieving cancelled eventsevent_type- this filter is Google-only
Combining filters works the way you’d expect. This example pulls events in a specific time range:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&title=standup&start=1706000000&end=1706100000&limit=10" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const events = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", title: "standup", start: 1706000000, end: 1706100000, limit: 10, },});events = nylas.events.list( grant_id, query_params={ "calendar_id": "primary", "title": "standup", "start": 1706000000, "end": 1706100000, "limit": 10, })Things to know about Exchange
Section titled “Things to know about Exchange”Exchange on-prem behaves differently from Exchange Online (Microsoft Graph) in several ways that matter for calendar integrations.
Tentative events
Section titled “Tentative events”Exchange treats tentative events as busy by default when calculating availability, the same behavior as Microsoft Graph. The tentative_as_busy query parameter controls this. If your app needs to distinguish between confirmed and tentative events, check the status field on each event object rather than relying on the default availability calculation.
No cancelled event retrieval
Section titled “No cancelled event retrieval”The show_cancelled parameter is not supported on EWS. When an organizer cancels an event or removes an occurrence from a recurring series, Exchange deletes it entirely rather than marking it as cancelled. You cannot retrieve cancelled events from Exchange on-prem accounts through the Nylas API.
This is important if you’re building audit or compliance features that need to track event cancellations. Consider using webhooks to capture deletion events in real time before they become unrecoverable.
Recurring event restrictions
Section titled “Recurring event restrictions”Microsoft Exchange has specific constraints around recurring events that don’t apply to Google Calendar:
- No overlapping instances. You cannot reschedule an instance of a recurring event to fall on the same day as, or the day before, the previous instance. Exchange rejects the update to prevent overlapping occurrences within a series.
- Overrides removed on recurrence change. If you modify the recurrence pattern of a series (for example, changing from weekly to daily), Exchange removes all existing overrides. Google keeps them if they still fit the new pattern.
- EXDATE recovery is not possible. Once you remove an occurrence from a recurring series, there is no way to restore it. You would need to create a standalone event to fill the gap.
- No multi-day monthly BYDAY. You cannot create a monthly recurring event on multiple days of the week (like the first and third Thursday). Exchange’s recurrence model does not support different indices within a single rule.
For the full breakdown of provider-specific recurring event behavior, see Recurring events.
On-prem networking
Section titled “On-prem networking”The Exchange server’s EWS endpoint must be reachable from Nylas infrastructure. This is the most common source of connection failures for on-prem deployments.
- EWS must be enabled on the server and exposed outside the corporate network
- If the server is behind a firewall, you need to allow Nylas’s IP addresses (available on contract plans with static IPs)
- A reverse proxy in front of the Exchange server is a common workaround if direct firewall rules are not feasible
- Accounts in admin groups are not supported
If calendar data is not syncing for an Exchange account, verify that the EWS endpoint is accessible before investigating other causes.
Autodiscovery
Section titled “Autodiscovery”Nylas uses Exchange autodiscovery to locate the EWS endpoint automatically during authentication. This works well when autodiscovery is properly configured on the Exchange server. When it is not, users must manually provide the server address.
If users report authentication failures, the Exchange administrator can test autodiscovery using Microsoft’s Remote Connectivity Analyzer. Misconfigured autodiscovery is one of the most common issues with Exchange on-prem integrations.
Timezone handling
Section titled “Timezone handling”Exchange stores timezone information using Windows timezone identifiers like “Eastern Standard Time” or “Pacific Standard Time.” Nylas normalizes these to IANA identifiers (like “America/New_York” or “America/Los_Angeles”) automatically, so event times in API responses always use IANA format. You do not need to maintain a Windows-to-IANA mapping table in your application.
Sync timing
Section titled “Sync timing”Calendar sync performance for Exchange on-prem depends on the EWS server’s responsiveness and network latency between Nylas infrastructure and the Exchange server. On-prem servers with high load or limited bandwidth may introduce noticeable sync delays compared to cloud-hosted Exchange.
For apps that need real-time awareness of calendar changes, use webhooks instead of polling. Nylas pushes a notification to your server as soon as the event syncs, regardless of the underlying server speed.
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 cannot predict what they will 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 calendars frequently, webhooks are the best way to avoid hitting rate limits. Let Nylas notify you of changes instead of polling.
Paginate through results
Section titled “Paginate through results”The Events 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>/events?calendar_id=primary&limit=10&page_token=<NEXT_CURSOR>" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'let pageCursor = undefined;
do { const result = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", limit: 10, pageToken: pageCursor, }, });
// Process result.data here
pageCursor = result.nextCursor;} while (pageCursor);page_cursor = None
while True: query = {"calendar_id": "primary", "limit": 10} if page_cursor: query["page_token"] = page_cursor
result = nylas.events.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”- Events API reference for full endpoint documentation and all available parameters
- Using the Events API for creating, updating, and deleting events
- Recurring events for series creation, overrides, and provider-specific behavior
- Availability to check free/busy across multiple calendars
- Webhooks for real-time notifications instead of polling
- Exchange on-premises provider guide for full Exchange setup including authentication and network requirements
- Microsoft guide for cloud-hosted Exchange (Microsoft 365, Exchange Online)