# Using virtual calendars

Source: https://developer.nylas.com/docs/v3/calendar/virtual-calendars/

Virtual calendars work like any other calendar in Nylas. They also make it easy to include customized scheduling in your project by allowing users to schedule events without needing to connect to a third-party provider, like Google or Microsoft.

You might prefer virtual calendars for your project if...

- Your users have sensitive information on their personal calendars that they don't want to expose to complete their task.
- Your users don't have accounts on the providers your project supports (for example, external contractors).
- You want to let users book resources that don't have accounts on the providers your project supports (for example, meeting rooms).

## How virtual calendars work

Nylas' virtual calendars are linked to virtual accounts within your Nylas application. Unlike normal grants, virtual accounts don't expire, they don't generate [`grant.expired` notifications](/docs/reference/notifications/grants/grant-expired/), and you don't need to define or manage their scopes. Each Nylas application can have as many virtual accounts as necessary, billed at your usual per-grant price. Each virtual account can have up to 10 virtual calendars.

> **Info:** 
> **You can only use the [**Manage Grants**](/docs/reference/api/manage-grants/), [**Calendar**](/docs/reference/api/calendar/), and [**Events**](/docs/reference/api/events/) endpoints with virtual accounts**. They don't have access to the Email and Contacts APIs.

Virtual accounts are displayed in the [Nylas Dashboard](https://dashboard-v3.nylas.com/?utm_source=docs&utm_content=virtual-calendars) as grants. Their provider is always "Virtual calendar".

![The Nylas Dashboard showing a list of grants. The virtual calendar's Provider is listed as Virtual Calendar.](/_images/calendar/virtual-calendar/v3-virtual-calendar.png "Virtual calendars in Nylas")

The first virtual calendar that you create for a virtual account becomes the account's primary calendar. Like third-party calendars, you can reference the virtual calendar in API requests using the `primary` keyword instead of the `calendar_id`. You can't change which virtual calendar is the primary, and you can't delete the primary calendar.

You can use virtual calendars with [Scheduler](/docs/v3/scheduler/) to book events.

## Authenticate a virtual account

There are two steps to authenticating a virtual account: first, you have to [create a virtual calendar connector](#create-a-virtual-calendar-connector), then [create a virtual account](#create-a-virtual-account).

### Create a virtual calendar connector

To create a virtual calendar connector in the [Nylas Dashboard](https://dashboard-v3.nylas.com/?utm_source=docs&utm_content=virtual-calendars), select **Connectors** in the left navigation, scroll to the **Virtual calendar** option, and click the **plus symbol** beside it.

![A close-up of the Nylas Dashboard Connectors page displaying the Virtual Calendar connector entry.](/_images/calendar/virtual-calendar/virtual-calendar-connector.png "Create a virtual calendar connector")

Programmatically, you can create a virtual calendar connector by making a [Create Connector request](/docs/reference/api/connectors-integrations/create_connector/).

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/connectors" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data '{
    "provider": "virtual-calendar"
  }'

```

```js [createConnector-Node.js]

import Nylas from "nylas";

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

async function createConnector() {
  try {
    const connector = await nylas.connectors.create({
      requestBody: {
        name: "nylas",
        provider: "virtual-calendar",
      },
    });

    console.log("Connector created:", connector);
  } catch (error) {
    console.error("Error creating provider:", error);
  }
}

createConnector();


```

```python
from nylas import Client

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

connector = nylas.connectors.create(
    request_body={
      "name": 'nylas',
      "provider": "virtual-calendar"
    }
)

print(connector)

```

```ruby
require 'nylas'

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

request_body = {
  name: 'Nylas',
  provider: 'virtual-calendar'
}

begin
  nylas.connectors.create(request_body: request_body)
rescue Exception => exception
  puts exception
end

```

```kt
import com.nylas.NylasClient
import com.nylas.models.CreateConnectorRequest

fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
  val request = CreateConnectorRequest.VirtualCalendar()
  val connector = nylas.connectors().create(request)

  print(connector.data)
}

```

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

public class connector {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
    CreateConnectorRequest request = new CreateConnectorRequest.VirtualCalendar();
    Response<Connector> connectorResponse = nylas.connectors().create(request);

    System.out.println(connectorResponse);
  }
}

```

### Create a virtual account

Make a [BYO Authentication request](/docs/reference/api/manage-grants/byo_auth/) and set the `provider` to `virtual-calendar`.

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/connect/custom' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'\
  --header 'Content-Type: application/json' \
  --data '{
    "provider": "virtual-calendar",
    "settings": {
      "email": "Nyla-virtual-account"
    }
  }'
```

```json {4} [createAccount-Response (JSON)]
{
  "request_id": "5967ca40-a2d8-4ee0-a0e0-6f18ace39a90",
  "data": {
    "id": "<NYLAS_GRANT_ID>",
    "provider": "virtual-calendar",
    "grant_status": "valid",
    "email": "Nyla-virtual-account",
    "scope": [],
    "user_agent": "string",
    "ip": "string",
    "state": "<STATE>",
    "created_at": 1617817109,
    "updated_at": 1617817109
  }
}
```

```js [createAccount-Node.js]

import Nylas from "nylas";

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

async function createVirtualCalendarGrant() {
  try {
    const response = await nylas.auth.customAuthentication({
      requestBody: {
        provider: "virtual-calendar",
        settings: {
          email: "devrel-virtual-calendar",
        },
        scope: ["calendar"],
      },
    });

    console.log("Grant created:", response);
  } catch (error) {
    console.error("Error creating grant:", error);
  }
}

createVirtualCalendarGrant();


```

```python
from nylas import Client

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

grant_id = nylas.auth.custom_authentication(
    request_body={
      "provider": "virtual-calendar",
      "settings": {
        "email": 'devrel-virtual-calendar',
      },
      "scope": ['calendar']
    }
)

print(grant_id)

```

```ruby
# frozen_string_literal: true

require 'nylas'
require 'sinatra'

set :show_exceptions, :after_handler

error 404 do
  'No authorization code returned from Nylas'
end

error 500 do
  'Failed to exchange authorization code for token'
end

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

get '/nylas/auth' do
  request_body = {
    provider: 'virtual-calendar',
      settings: {
        email: "nylas-virtual-calendar" 
      }
    }

    response = nylas.auth.custom_authentication(request_body)
    "#{response}"
end

```

```kt
import com.nylas.NylasClient
import com.nylas.models.*
import spark.Spark.get

fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")

  get("/nylas/auth") { _, _ ->
    val settings = mapOf<String, Any>("email" to "nylas-virtual-calendar")
    val scopes = listOf("openapi")

    val requestBody = CreateGrantRequest.Builder(AuthProvider.VIRTUAL_CALENDAR, settings)
        .scopes(scopes)
        .state("xyz")
        .build()

    val authResponse: Response<Grant> = nylas.auth().customAuthentication(requestBody)
    authResponse
  }
}

```

```java
import java.util.*;
import static spark.Spark.*;
import com.nylas.NylasClient;
import com.nylas.models.*;

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

    get("/nylas/auth", (request, response) -> {
      Map<String, String> settings = new HashMap<>();
      settings.put("email", "nylas-virtual-calendar");

      List<String> scopes = new ArrayList<>();
      scopes.add("openid");

      CreateGrantRequest requestBody = new CreateGrantRequest.
          Builder(AuthProvider.VIRTUAL_CALENDAR, settings).
          scopes(scopes).state("xyz").
          build();

      Response<Grant> authResponse = nylas.auth().customAuthentication(requestBody);

      return "%s".formatted(authResponse);
    });
  }
}

```

The `settings.email` field can be any arbitrary string — it doesn't need to be formatted as an email address. Nylas uses this value as an ID to manage the virtual account. Because of this, you can't use the identifier listed in `settings.email` to authenticate a third-party account with Nylas. We strongly recommend against using an existing email address as the identifier.

## Create a virtual calendar

To create a virtual calendar, make a [Create Calendar request](/docs/reference/api/calendar/create-calendar/) that specifies the grant ID of an existing virtual account.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/calendars" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data '{
    "name": "DevRel calendar",
    "description": "Nylas Developer Relations calendar."  
  }'

```

```js [createVirtualCalendar-Node.js]

import Nylas from "nylas";

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

async function createVirualCalendar() {
  try {
    const calendar = await nylas.calendars.create({
      identifier: "<NYLAS_GRANT_ID>",
      requestBody: {
        name: "Nylas DevRel",
        description: "Nylas Developer Relations",
      },
    });

    console.log("Virtual Calendar:", calendar);
  } catch (error) {
    console.error("Error to create virtual calendar:", error);
  }
}

createVirualCalendar();


```

```python
from nylas import Client

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

grant_id = "<NYLAS_GRANT_ID>"

calendar = nylas.calendars.create(
  grant_id,
  request_body={
    "name": 'Nylas DevRel',
    "description": 'Nylas Developer Relations'
  }
)

print(calendar)

```

```ruby
require 'nylas' 

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

request_body = {
  "name": "Nylas DevRel",
  "description": "Nylas Developer Relations",
  "timezone": "America/Toronto"
}

begin
  calendars, _request_ids = nylas.calendars.create(
      identifier: '<VIRTUAL_CALENDAR_ID>', 
      request_body: request_body)

  puts calendars
rescue Exception => exception
  puts exception
end


```

```kt
import com.nylas.NylasClient
import com.nylas.models.*

fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")

  val requestBody = CreateCalendarRequest(
      "Nylas DevRel",
      "Nylas Developer Relations",
      "Nylas Headquarters",
      "America/Toronto",
      mapOf<String, String>()
  )

  val calendar: Response<Calendar> = nylas.calendars().create(
      "<VIRTUAL_CALENDAR_GRANT_ID>", 
      requestBody)
      
  print(calendar.data)
}


```

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

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

    CreateCalendarRequest requestBody = new CreateCalendarRequest.Builder("Nylas DevRel")
        .description("Nylas Developer Relations")
        .location("Nylas Headquarters")
        .timezone("America/Toronto")
        .build();

    try {
      Response<Calendar> calendar = nylas.calendars().create(
          "<VIRTUAL_CALENDAR_GRANT_ID>", 
          requestBody);

      System.out.println(calendar);
    } catch(Exception e) {
      System.out.printf(" %s%n", e);
    }
  }
}


```

## Create a virtual calendar event

> **Warn:** 
> **Nylas doesn't send email invitations or event reminders to participants on virtual calendar events**.

Now that you have both a virtual account and a virtual calendar, you can start creating events. Make a [Create Event request](/docs/reference/api/events/create-event/) that specifies the virtual account's grant ID and the virtual calendar ID.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data '{
    "when": {
      "start_time": 1704302073,
      "end_time": 1704305673
    },
    "title": "Build With Nylas"
  }'
```

### Set event visibility

You can set the `visibility` field on virtual calendar events to control whether they appear as `private` or `public`, aligning with native provider behavior on Google and Microsoft calendars. The `visibility` field is optional — if you don't set it, the event defaults to `public` behavior.

You can set `visibility` when you create an event or update it later. When you read events, the `visibility` field only appears in responses if it was explicitly set.

```bash [eventVisibility-Create with visibility]
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data '{
    "when": {
      "start_time": 1704302073,
      "end_time": 1704305673
    },
    "title": "Build With Nylas",
    "visibility": "private"
  }'
```

```bash [eventVisibility-Update visibility]
curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events/<EVENT_ID>?calendar_id=<CALENDAR_ID>" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data '{
    "visibility": "public"
  }'
```

```js [createEvent-Node.js]

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
from nylas import Client

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

grant_id = "<NYLAS_GRANT_ID>"
calendar_id = "<CALENDAR_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
require 'nylas'

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

query_params = {
  calendar_id: '<VIRTUAL_CALENDAR_ID>'
}

start_time = Time.now.to_i
end_time = start_time + 3600

request_body = {
  when: {
    start_time: start_time,
    end_time: end_time
  },
  title: "Build With Nylas",
}

events, _request_ids = nylas.events.create(
    identifier: '<VIRTUAL_CALENDAR_GRANT_ID>',
    query_params: query_params, 
    request_body: request_body)
    
if _request_ids != "" 
  puts events[:id]
  puts events[:title]
  puts "Event created successfully"
else
  puts "There was an error creating the event"
end


```

```kt
import com.nylas.NylasClient
import com.nylas.models.*

fun main(args: Array<String>) {
	val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")

	// Replace startTime/endTime with your event window as Unix timestamps (seconds since epoch).
	val startTime = 1714640910
	val endTime = 1714644510
	val eventWhenObj: CreateEventRequest.When = CreateEventRequest.When.Timespan(startTime, endTime)

	val eventRequest: CreateEventRequest = CreateEventRequest.Builder(eventWhenObj).
			title("Nylas DevRel").
			description("Nylas Developer Relations").
			location("Nylas Headquarters").
			build()

	val eventQueryParams: CreateEventQueryParams = CreateEventQueryParams("<VIRTUAL_CALENDAR_ID>")

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


```

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

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

		// Replace startTime/endTime with your event window as Unix timestamps (seconds since epoch).
		int startTime = 1714640910;
		int endTime = 1714644510;
		CreateEventRequest.When.Timespan timespan = new CreateEventRequest.When.Timespan.
				Builder(startTime, endTime).
				build();

		CreateEventRequest request = new CreateEventRequest.Builder(timespan).
				title("Build With Nylas").
				build();

		CreateEventQueryParams queryParams = new CreateEventQueryParams.Builder("<VIRTUAL_CALENDAR_ID>").build();
		
		Response<Event> event = nylas.events().create(
				"<VIRTUAL_CALENDAR_GRANT_ID>",
				request,
				queryParams);
				
		System.out.println(event);
	}
}


```

## Get virtual calendar availability

You can use the [Get Availability endpoint](/docs/reference/api/calendar/post-availability/) to get availability information for a virtual calendar.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/calendars/availability" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "participants": [{
      "email": "Nyla-virtual-account",
      "calendar_ids": ["primary"],
      "open_hours": [{
        "days": [0,1,2],
        "timezone": "America/Toronto",
        "start": "9:00",
        "end": "17:00",
        "exdates": []
      }]
    }],
    "start_time": 1600890600,
    "end_time": 1600999200,
    "interval_minutes": 30,
    "duration_minutes": 30,
    "round_to": 15,
    "availability_rules": {
      "availability_method": "max-availability",
      "buffer": {
        "before": 15,
        "after": 15
      },
      "default_open_hours": [
        {
          "days": [0,1,2],
          "timezone": "America/Toronto",
          "start": "9:00",
          "end": "17:00",
          "exdates": []
        },
        {
          "days": [3,4,5],
          "timezone": "America/Toronto",
          "start": "10:00",
          "end": "18:00",
          "exdates": []
        }
      ]
    }
  }'
```

```json [availability-Response (JSON)]
{
  "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88",
  "data": {
    "time_slots": [
      {
        "emails": ["Nyla-virtual-account"],
        "start_time": 1659367800,
        "end_time": 1659369600
      },
      {
        "emails": ["Nyla-virtual-account"],
        "start_time": 1659376800,
        "end_time": 1659378600
      }
    ]
  }
}
```

## Virtual calendar notifications

Virtual calendars support [`calendar.created`](/docs/reference/notifications/calendar/calendar-created/), [`event.created`](/docs/reference/notifications/events/event-created/), and [`event.updated`](/docs/reference/notifications/events/event-updated/) notifications.