# How to create Exchange calendar events

Source: https://developer.nylas.com/docs/cookbook/calendar/events/create-events-ews/

Exchange on-premises servers remain widespread in enterprise environments, particularly in regulated industries, government, and organizations that have not yet migrated to Microsoft 365. Creating calendar events on these servers means talking to Exchange Web Services (EWS), a SOAP-based XML protocol that predates modern REST APIs.

Nylas abstracts the EWS complexity behind the same [Events API](/docs/reference/api/events/) you use for Google Calendar, Outlook, and iCloud. You send a JSON payload, and Nylas translates it into the correct EWS SOAP envelope, handles autodiscovery, and manages credentials. This guide covers the EWS-specific details you need to know when creating events on Exchange on-prem.

## 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](/docs/cookbook/calendar/events/create-events-microsoft/) instead. The `ews` connector is specifically for organizations that run their own Exchange servers.

> **Warn:** 
> **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?

Creating a calendar event through EWS means constructing a SOAP XML envelope with deeply nested elements for the event title, body, start and end times, timezone definitions, attendees, recurrence patterns, and reminders. A single create-event call can easily exceed 50 lines of XML. You also need to handle autodiscovery to find the correct EWS endpoint (which is frequently misconfigured), manage credential-based authentication with support for two-factor app passwords, parse SOAP fault responses when something goes wrong, and build retry logic around Exchange's admin-configured throttling policies.

Nylas replaces all of that with a single `POST` request containing a JSON body. No XML, no WSDL, no SOAP. Authentication, autodiscovery, and timezone conversion are handled automatically. Your event-creation code stays the same whether you target 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

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key
- A [grant](/docs/v3/auth/) for an Exchange on-premises account
- An EWS connector configured with the `ews.calendars` scope (read and write access)
- The Exchange server accessible from outside the corporate network (not behind a VPN or firewall that blocks external access)


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


### 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 `user@example.com` 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`).

> **Info:** 
> **Users with two-factor authentication** must generate an app password instead of using their regular password. See [Microsoft's app password documentation](https://support.microsoft.com/en-us/help/12409/) for instructions.

The full setup walkthrough is in the [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/).

## Create an event

Make a [Create Event request](/docs/reference/api/events/create-event/) with the grant ID and a `calendar_id`. You can use `primary` as the `calendar_id` to target the account's default calendar.

> **Error:** 
> **You're about to send a real event invite!** The following samples send an email from the account you connected to the Nylas API to any email addresses you put in the `participants` sub-object. Make sure you actually want to send this invite to those addresses before running this command!

```bash
curl --compressed --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Annual Philosophy Club Meeting",
    "busy": true,
    "conferencing": {
        "provider": "Zoom Meeting",
        "autocreate": {
          "conf_grant_id": "<NYLAS_GRANT_ID>",
          "conf_settings": {
            "settings": {
              "join_before_host": true,
              "waiting_room": false,
              "mute_upon_entry": false,
              "auto_recording": "none"
            }
          }
        }
      },
    "participants": [
      {
        "name": "Leyah Miller",
        "email": "leyah@example.com"
      },
      {
        "name": "Nyla",
        "email": "nyla@example.com"
      }
    ],
    "resources": [{
      "name": "Conference room",
      "email": "conference-room@example.com"
    }],
    "description": "Come ready to talk philosophy!",
    "when": {
      "start_time": 1674604800,
      "end_time": 1722382420,
      "start_timezone": "America/New_York",
      "end_timezone": "America/New_York"
    },
    "location": "New York Public Library, Cave room",
    "recurrence": [
      "RRULE:FREQ=WEEKLY;BYDAY=MO",
      "EXDATE:20211011T000000Z"
  ],
}'

```

```json
{
  "request_id": "1",
  "data": {
    "busy": true,
    "calendar_id": "primary",
    "conferencing": {
      "details": {
        "meeting_code": "<MEETING_CODE>",
        "url": "<MEETING_URL>"
      },
      "provider": "Google Meet"
    },
    "created_at": 1701974804,
    "creator": {
      "email": "leyah@example.com",
      "name": "Leyah Miller"
    },
    "description": null,
    "grant_id": "<NYLAS_GRANT_ID>",
    "hide_participants": false,
    "html_link": "<EVENT_LINK>",
    "ical_uid": "6aaaaaaame8kpgcid6hvd0q@google.com",
    "id": "<NYLAS_EVENT_ID>",
    "object": "event",
    "organizer": {
      "email": "leyah@example.com",
      "name": "Leyah Miller"
    },
    "participants": [
      {
        "email": "jenna.doe@example.com",
        "status": "yes"
      },
      {
        "email": "leyah@example.com",
        "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"
    }
  }
}


```

```js [createEvents-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

const now = Math.floor(Date.now() / 1000); // Time in Unix timestamp format (in seconds)

async function createAnEvent() {
  try {
    const event = await nylas.events.create({
      identifier: "<NYLAS_GRANT_ID>",
      requestBody: {
        title: "Build With Nylas",
        when: {
          startTime: now,
          endTime: now + 3600,
        },
      },
      queryParams: {
        calendarId: "<CALENDAR_ID>",
      },
    });

    console.log("Event:", event);
  } catch (error) {
    console.error("Error creating event:", error);
  }
}

createAnEvent();


```

```python [createEvents-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

events = nylas.events.create(
  grant_id,
  request_body={
    "title": 'Build With Nylas',
    "when": {
      "start_time": 1609372800,
      "end_time": 1609376400
    },
  },
  query_params={
    "calendar_id": "<CALENDAR_ID>"
  }
)

print(events)

```


```ruby [createEvents-Ruby SDK]

require 'nylas'
require 'date'

nylas = Nylas::Client.new(api_key: "<NYLAS_API_KEY>")

query_params = {
  calendar_id: "<CALENDAR_ID>"
}

today = Date.today
start_time = Time.local(today.year, today.month, today.day, 13, 0, 0).to_i
end_time = Time.local(today.year, today.month, today.day, 13, 30, 0).to_i

request_body = {
  when: {
    start_time: start_time,
    end_time: end_time
  },
  title: "Let's learn some Nylas Ruby SDK!",
  location: "Nylas' Headquarters",
  description: "Using the Nylas API with the Ruby SDK is easy.",
  participants: [{
    name: "Blag",
    email: "atejada@gmail.com",
    status: 'noreply'
  }]
}

event, _request_id = nylas.events.create(
  identifier: "<NYLAS_GRANT_ID>",
  query_params: query_params,
  request_body: request_body
)

puts event


```

```java [createEvents-Java SDK]

import com.nylas.NylasClient;
import com.nylas.models.*;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.*;

public class create_calendar_events {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();

    // Get today's date
    LocalDate today = LocalDate.now();

    // Set time. Because we're using UTC we need to add the hours in difference from our own timezone.
    Instant sixPmUtc = today.atTime(13, 0).toInstant(ZoneOffset.UTC);

    // Set the date and time for the event. We add 30 minutes to the starting time.
    Instant sixPmUtcPlus = sixPmUtc.plus(30, ChronoUnit.MINUTES);

    // Get the Date and Time as a Unix timestamp
    long startTime = sixPmUtc.getEpochSecond();
    long endTime = sixPmUtcPlus.getEpochSecond();

    // Define title, location, and description of the event
    String title = "Let's learn some about the Nylas Java SDK!";
    String location = "Nylas Headquarters";
    String description = "Using the Nylas API with the Java SDK is easy.";

    // Create the timespan for the event
    CreateEventRequest.When.Timespan timespan = new CreateEventRequest.
        When.Timespan.
        Builder(Math.toIntExact(startTime), Math.toIntExact(endTime)).
        build();

    // Create the list of participants.
    List<CreateEventRequest.Participant> participants_list = new ArrayList<>();

    participants_list.add(new CreateEventRequest.
        Participant("johndoe@example.com", ParticipantStatus.NOREPLY,
        "John Doe", "", ""));

    // Build the event details.
    CreateEventRequest createEventRequest = new CreateEventRequest.Builder(timespan)
        .participants(participants_list)
        .title(title)
        .location(location)
        .description(description)
        .build();

    // Build the event parameters. In this case, the Calendar ID.
    CreateEventQueryParams createEventQueryParams = new CreateEventQueryParams.Builder("<CALENDAR_ID>").build();

    // Create the event itself
    Event event = nylas.events().create(
        "<NYLAS_GRANT_ID>",
        createEventRequest,
        createEventQueryParams).getData();
  }
}

```

```kt [createEvents-Kotlin SDK]

import com.nylas.NylasClient
import com.nylas.models.*

import java.time.LocalDateTime
import java.time.ZoneOffset

fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
  var startDate = LocalDateTime.now()

  // Set the time. Because we're using UTC, we need to add the difference in hours from our own timezone.
  startDate = startDate.withHour(13);
  startDate = startDate.withMinute(0);
  startDate = startDate.withSecond(0);
  val endDate = startDate.withMinute(30);

  // Convert the dates from Unix timestamp format to integer.
  val iStartDate: Int = startDate.toEpochSecond(ZoneOffset.UTC).toInt()
  val iEndDate: Int = endDate.toEpochSecond(ZoneOffset.UTC).toInt()

  // Create the timespan for the event.
  val eventWhenObj: CreateEventRequest.When = CreateEventRequest.When.
  Timespan(iStartDate, iEndDate);

  // Define the title, location, and description of the event.
  val title: String = "Let's learn about the Nylas Kotlin/Java SDK!"
  val location: String = "Blag's Den!"
  val description: String = "Using the Nylas API with the Kotlin/Java SDK is easy."

  // Create the list of participants.
  val participants: List<CreateEventRequest.Participant> = listOf(CreateEventRequest.
      Participant("<PARTICIPANT_EMAIL>", ParticipantStatus.NOREPLY, "<PARTICIPANT_NAME>"))

  // Create the event request. This adds date/time, title, location, description, and participants.
  val eventRequest: CreateEventRequest = CreateEventRequest(eventWhenObj, title, location, description, participants)

  // Set the event parameters.
  val eventQueryParams: CreateEventQueryParams = CreateEventQueryParams("<CALENDAR_ID>")

  val event: Response<Event> = nylas.events().create("<NYLAS_GRANT_ID>",
      eventRequest, eventQueryParams)
}

```


The `calendar_id=primary` shortcut works for EWS accounts, targeting the user's default calendar. The response format is identical across providers, so your event-creation logic works the same for Exchange on-prem, Exchange Online, Google, and iCloud.

## Add participants and send invitations

When you include a `participants` array in your create request, Exchange handles the meeting invitations through its internal mail transport. The `notify_participants` query parameter controls whether invitations are sent:

- `notify_participants=true` (the default) sends a meeting invitation to every address in the `participants` array. Exchange delivers these through its own transport, not through a separate email send.
- `notify_participants=false` creates the event on the organizer's calendar without notifying anyone. Participants do not receive a message or an ICS file, and the event does not appear on their calendars.

This behavior is consistent with how Exchange handles meeting requests natively. One thing to watch for: if the participant is on the same Exchange server, the event may appear on their calendar almost instantly. External participants receive a standard ICS invitation email.

## Things to know about Exchange

Exchange on-prem behaves differently from Exchange Online (Microsoft Graph) in several ways that matter when creating calendar events.

### EWS connector scope

The `ews.calendars` scope on your EWS connector grants both read and write access to the Calendar API. Without this scope, create requests fail with a permissions error. You can configure scopes when setting up the connector:

| Scope           | Access                                |
| --------------- | ------------------------------------- |
| `ews.messages`  | Email API (messages, drafts, folders) |
| `ews.calendars` | Calendar API                          |
| `ews.contacts`  | Contacts API                          |

### No conferencing auto-create

EWS does not support automatically creating conferencing links (Teams, Zoom, or otherwise) when you create an event. If you need a video conferencing link on the event, generate it through the conferencing provider's API first, then include the URL in the event's `location` or `description` field. This is a platform limitation of Exchange on-prem, not a Nylas restriction.

### Recurring event restrictions

Microsoft Exchange has specific constraints around recurring events that do not 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](/docs/v3/calendar/recurring-events/).

### Timezone handling

Nylas accepts IANA timezone identifiers (like `America/New_York` or `Europe/London`) in your create request. You do not need to convert to Windows timezone IDs like "Eastern Standard Time." Nylas handles the translation to Exchange's internal format automatically.

### Room resources

Exchange supports booking room resources through the `resources` field on an event. If the room is configured as an Exchange resource mailbox, you can include it as a resource when creating the event. The resource mailbox's auto-accept policy determines whether the room is automatically confirmed or requires approval.

### 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](/docs/dev-guide/platform/#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 event creation is failing for an Exchange account, verify that the EWS endpoint is accessible before investigating other causes.

### Rate limits

Unlike Google and Microsoft's cloud services, Exchange on-prem rate limits are set by the server administrator. Write operations like event creation may be more restricted than read operations. Nylas cannot predict what the limits 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 create events frequently, consider batching operations and building backoff logic around the `Retry-After` response.

### Sync timing

Created events depend 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 delays before the event appears in sync results.

For apps that need confirmation that an event was created successfully, use [webhooks](/docs/v3/notifications/) to receive a notification as soon as the event syncs. This is more reliable than polling.

## What's next

- [Events API reference](/docs/reference/api/events/) for full endpoint documentation and all available parameters
- [Using the Events API](/docs/v3/calendar/using-the-events-api/) for updating, deleting, and managing events
- [List Exchange events](/docs/cookbook/calendar/events/list-events-ews/) for reading events from Exchange on-prem accounts
- [Recurring events](/docs/v3/calendar/recurring-events/) for series creation, overrides, and provider-specific behavior
- [Availability](/docs/v3/calendar/calendar-availability/) to check free/busy across multiple calendars
- [Webhooks](/docs/v3/notifications/) for real-time notifications when events are created or updated
- [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/) for full Exchange setup including authentication and network requirements
- [Microsoft create events guide](/docs/cookbook/calendar/events/create-events-microsoft/) for cloud-hosted Exchange (Microsoft 365, Exchange Online)