# Schedule rooms and resources with Agent Accounts

Source: https://developer.nylas.com/docs/cookbook/calendar/schedule-resources-with-agent-accounts/

A conference room, a company van, or a contractor who does shifts for you all need a calendar, and none of them has an account on Google or Microsoft to connect. If your organization already registers rooms in Google Workspace or Microsoft 365, [book them as provider resources](/docs/cookbook/calendar/book-room-resources/). For everything else, give the resource a [Nylas Agent Account](/docs/v3/agent-accounts/).

An Agent Account is a Nylas-hosted mailbox and calendar at an address you choose, such as `room-a@rooms.yourcompany.com`. You create it with one request, book it through the same Events API you use for people, and because it has a real address, anyone in your company can invite it straight from their own calendar app. This recipe sets one up for a meeting room and covers both booking paths.

## How does an Agent Account work as a resource calendar?

An Agent Account is a grant like any other, with a primary calendar that Nylas provisions when it creates the account. There's no OAuth flow, no scopes to request, and the grant never expires. Every one of the Calendar and Events endpoints in the [Agent Account endpoint list](/docs/v3/agent-accounts/supported-endpoints/#calendars) works on it.

The difference from a plain hosted calendar is the mailbox behind it. When someone adds `room-a@rooms.yourcompany.com` to a meeting in Google Calendar, Outlook, or Apple Calendar, the invitation arrives in the room's mailbox and Nylas turns it into an event on the room's calendar. Your code decides whether the room is free and answers through the [Send RSVP endpoint](/docs/reference/api/events/send-rsvp/), and the organizer sees the room accept or decline next to every other attendee. When your application books the room directly and adds the requester as a participant, the room sends them the invitation, and any later change or cancellation reaches their calendar too.

## Create an Agent Account for the room

Agent Accounts live on a domain registered with Nylas. A trial `*.nylas.email` subdomain takes no DNS work, and a custom subdomain such as `rooms.yourcompany.com` needs the MX and TXT records in [Set up domains for Agent Accounts](/docs/v3/agent-accounts/dns-provider-setup/). Putting every room on its own subdomain also groups them into one [workspace](/docs/dev-guide/platform/workspaces/), so a single policy covers all of them.

With the domain verified, the [Nylas CLI](/docs/v3/getting-started/cli/) creates the account in one command, and the primary calendar is ready as soon as it returns. The top-level `name` becomes the display name on every invitation the room sends.

```bash
nylas agent account create room-a@rooms.yourcompany.com
```

The same request through the API uses [custom authentication](/docs/reference/api/manage-grants/byo_auth/) with `provider` set to `nylas`. Save the `id` in the response, which is the grant ID for every later call.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Conference Room A",
    "settings": {
      "email": "room-a@rooms.yourcompany.com"
    }
  }'
```

```json [createRoom-Response (JSON)]
{
  "request_id": "5967ca40-a2d8-4ee0-a0e0-6f18ace39a90",
  "data": {
    "id": "<ROOM_GRANT_ID>",
    "provider": "nylas",
    "grant_status": "valid",
    "email": "room-a@rooms.yourcompany.com",
    "name": "Conference Room A",
    "scope": [],
    "created_at": 1742932766
  }
}
```

## Book the room from your application

To reserve the room from your own booking UI, create an event on the room's grant with `calendar_id=primary`. Add the person who booked it as a participant and set `notify_participants=true`, and the room sends them an invitation they can accept in their own calendar. Later `PUT` and `DELETE` calls on the event with the same flag update or cancel it on their calendar as well.

The request below books the room for one hour and invites the requester. Add a `metadata` object with your own booking ID, up to 50 key-value pairs, if you want to correlate the event with a row in your database later.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<ROOM_GRANT_ID>/events?calendar_id=primary&notify_participants=true" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "title": "Design review",
    "when": { "start_time": 1758016800, "end_time": 1758020400 },
    "participants": [
      { "email": "leyah@yourcompany.com", "name": "Leyah Miller" }
    ],
    "metadata": { "key1": "booking-4821" }
  }'
```

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


const nylas = new Nylas({
  apiKey: process.env.NYLAS_API_KEY,
  apiUri: "https://api.us.nylas.com",
});

const event = await nylas.events.create({
  identifier: process.env.ROOM_GRANT_ID,
  requestBody: {
    title: "Design review",
    when: { startTime: 1758016800, endTime: 1758020400 },
    participants: [{ email: "leyah@yourcompany.com", name: "Leyah Miller" }],
    metadata: { key1: "booking-4821" },
  },
  queryParams: {
    calendarId: "primary",
    notifyParticipants: true,
  },
});

console.log("Room booked:", event.data.id);
```

```python [bookRoom-Python SDK]

from nylas import Client

nylas = Client(os.environ["NYLAS_API_KEY"], "https://api.us.nylas.com")

event = nylas.events.create(
    os.environ["ROOM_GRANT_ID"],
    request_body={
        "title": "Design review",
        "when": {"start_time": 1758016800, "end_time": 1758020400},
        "participants": [{"email": "leyah@yourcompany.com", "name": "Leyah Miller"}],
        "metadata": {"key1": "booking-4821"},
    },
    query_params={"calendar_id": "primary", "notify_participants": True},
)

print("Room booked:", event.data.id)
```

## Let people book the room from their own calendar

The room's address works as an attendee anywhere. When an organizer adds it to a meeting in Google Calendar, Outlook, or Apple Calendar, the invitation lands in the room's mailbox, Nylas creates the matching event on the room's primary calendar, and an [`event.created`](/docs/reference/notifications/events/event-created/) webhook fires. The organizer is the person who sent the invite, and the room appears in `participants` with `status` set to `noreply`.

Nothing is accepted automatically. Your webhook handler applies whatever rules the room has, such as business hours, a capacity limit, or a check against the [Free/Busy endpoint](/docs/reference/api/calendar/post-calendars-free-busy/) for a conflicting booking, and then answers with one of three statuses: `yes`, `no`, or `maybe`. The reply goes back over standard iCalendar, so the organizer's calendar shows the room as accepted or declined within seconds, without them ever opening your application.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<ROOM_GRANT_ID>/events/<EVENT_ID>/send-rsvp?calendar_id=primary" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "status": "yes" }'
```

```js [roomRsvp-Node.js SDK]
const response = await nylas.events.sendRsvp({
  identifier: process.env.ROOM_GRANT_ID,
  eventId: "<EVENT_ID>",
  requestBody: { status: "yes" },
  queryParams: { calendarId: "primary" },
});
```

```python [roomRsvp-Python SDK]
response = nylas.events.send_rsvp(
    identifier=os.environ["ROOM_GRANT_ID"],
    event_id="<EVENT_ID>",
    request_body={"status": "yes"},
    query_params={"calendar_id": "primary"},
)
```

Subscribe to `event.created`, `event.updated`, and `event.deleted` for the room's grant so a reschedule or cancellation from the organizer's side reaches your handler the same way. [Sync calendar events with webhooks](/docs/cookbook/calendar/calendar-webhooks/) covers the subscription and payload shape.

## Check when a room is free

Before your application proposes a slot, ask the room's calendar for its busy blocks. The [Free/Busy endpoint](/docs/reference/api/calendar/post-calendars-free-busy/) on the room's grant takes a time window and the room's address, and returns every busy interval in it. The window below covers 2 days.

```bash
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<ROOM_GRANT_ID>/calendars/free-busy" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "start_time": 1758009600,
    "end_time": 1758182400,
    "emails": ["room-a@rooms.yourcompany.com"]
  }'
```

To find a slot that works for several rooms at once, or for a room plus the people attending, use the [availability endpoint](/docs/reference/api/calendar/post-availability/) with each address as a participant. [Find open meeting times across calendars](/docs/cookbook/calendar/find-meeting-times/) walks through that request.

## Offer a booking page with Scheduler

For a self-serve booking link, an Agent Account can own a [Scheduler](/docs/v3/scheduler/agent-accounts/) Configuration. Create the Configuration against the room's grant ID with `availability.calendar_ids` and `booking.calendar_id` both set to `primary`, and guests pick from the room's real availability on a Scheduling Page. Each booking lands on the room's calendar and the confirmation email comes from the room's own address.

All 4 [meeting types](/docs/v3/scheduler/meeting-types/) work, including round-robin pools that mix rooms with connected grants. Scheduler reads and writes the room's `primary` calendar only, so keep one Agent Account per bookable resource rather than several calendars on one account.

## Things to know about resource Agent Accounts

Every invitation, update, cancellation, and RSVP the room sends is an email, and each one counts against the account's [daily send quota](/docs/v3/agent-accounts/send-limits/#sending-limits), which is 200 messages per account per day on the free plan. A busy room that confirms 50 bookings a day is well inside that, but bulk backfills should pass `notify_participants=false` so they don't send at all. If a room is over quota, the event still saves and the invitation is skipped.

The room is a full mailbox, so an inbound invitation also fires [`message.created`](/docs/reference/notifications/messages/message-created/) for the invitation email itself. Drive your handler from `event.created` and ignore the message trigger, or attach a [policy](/docs/v3/agent-accounts/policies-rules-lists/) to the rooms' workspace to filter the inbox down to invitations only.

## What's next

- [How Agent Account calendars work](/docs/v3/agent-accounts/calendars/) for the full invitation, RSVP, and webhook behavior
- [Provisioning Agent Accounts](/docs/v3/agent-accounts/provisioning/) to create accounts in bulk, set display names, and assign workspaces
- [Use Scheduler with Agent Accounts](/docs/v3/scheduler/agent-accounts/) for booking pages, availability rules, and the `booking.*` webhook triggers
- [How to book a room for a meeting](/docs/cookbook/calendar/book-room-resources/) when your rooms are already provider resources in Google Workspace or Microsoft 365