# Calendar API

List and create events, answer invitations and find free meeting slots in your users' Google and Outlook calendars with one set of /v1/calendar endpoints.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Calendar API works on your users' own Google and Outlook calendars. Connect an account once, then list and create events, RSVP and find times when everyone is free, with the same `/v1/calendar` calls.

## What you can build

- **A booking page from free slots.** Offer the times when every host is free with `POST /v1/calendar/availability`, then book the chosen one with `POST /v1/calendar/events`.
- **An AI scheduling assistant.** Read the week with `GET /v1/calendar/events`, and accept or decline invitations with `POST /v1/calendar/events/{eventId}/rsvp`.
- **Video meetings from your app.** Create an event with a Google Meet or Microsoft Teams link in one request.
- **A team availability view.** Read busy blocks for up to 10 people with `POST /v1/calendar/free-busy`.
- **A sync into your own records.** Keep events current with the `calendar.event.created`, `calendar.event.updated` and `calendar.event.deleted` webhooks.

## Providers

<ProductPlatforms product="calendar" />

The calls are the same on both. They differ in the video meeting each one can attach:

| Provider | Calendars | Video meetings |
|---|---|---|
| <PlatformName slug="google" page="calendar" /> | Google Calendar | Google Meet, with `conferencing: "google_meet"` |
| <PlatformName slug="microsoft" page="calendar" /> | Outlook, Microsoft 365 and Outlook.com | Microsoft Teams, with `conferencing: "teams"` |

IMAP mailboxes have no calendar. Free-busy and availability take Google and Microsoft accounts in the same request.

## How it works

A calendar user is a connected account with its own `accountId`. Connect Google or Microsoft with OAuth and `calendar` in `scopes`. One account can grant `email`, `calendar` and `files` together.

Calls address the user's primary calendar unless you pass `calendarId`. `GET /v1/calendar/calendars` lists the others. Event ids are the provider's own.

Reads and writes go live to the provider. Each account counts as one [connected account](/pricing/social), whatever areas it grants.

## Your first request

You need an API key and the `accountId` of an account connected with `calendar`. This lists the events of one week.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.calendar.listCalendarEvents({
  query: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    from: '2026-11-16T00:00:00Z',
    to: '2026-11-23T00:00:00Z',
  },
});

for (const event of data.data) {
  console.log(event.title, event.start);
}
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

events = client.calendar.list_calendar_events(
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    from_="2026-11-16T00:00:00Z",
    to="2026-11-23T00:00:00Z",
)

for event in events["data"]:
    print(event["title"], event["start"])
```
</Tab>
<Tab value="curl">
```bash
curl "https://zernio.com/api/v1/calendar/events?accountId=66b2e19d8c3f5a7e9d0b1c2d&from=2026-11-16T00:00:00Z&to=2026-11-23T00:00:00Z" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```
</Tab>
</Tabs>

Response (`200`):

```json
{
  "data": [
    {
      "id": "6p1kq2rbc5i3eb9g",
      "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
      "provider": "google",
      "calendarId": "primary",
      "title": "Kickoff",
      "start": "2026-11-17T10:00:00+01:00",
      "end": "2026-11-17T10:30:00+01:00",
      "allDay": false,
      "participants": [{ "email": "client@example.com", "status": "accepted" }],
      "conferencing": { "provider": "google_meet", "url": "https://meet.google.com/abc-defg-hij" },
      "status": "confirmed"
    }
  ],
  "pagination": { "hasMore": false, "nextCursor": null }
}
```

Recurring series come back as one row per occurrence. Every parameter is on [List calendar events](/calendar/list-calendar-events).

## Examples

### Find slots for a booking page

Ask for 30-minute slots when two hosts are both free during working hours. Each slot lists the `accountIds` free in it.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data } = await zernio.calendar.calendarAvailability({
  body: {
    participants: [{ accountId: '66b2e19d8c3f5a7e9d0b1c2d' }, { accountId: '66b2e19d8c3f5a7e9d0b1c2e' }],
    from: '2026-11-16T00:00:00Z',
    to: '2026-11-20T00:00:00Z',
    durationMinutes: 30,
    defaultOpenHours: [{ days: [1, 2, 3, 4, 5], timezone: 'Europe/Madrid', start: '09:00', end: '17:00' }],
  },
});

console.log(data.data.timeSlots);
```
</Tab>
<Tab value="Python">
```python
slots = client.calendar.calendar_availability(
    participants=[{"accountId": "66b2e19d8c3f5a7e9d0b1c2d"}, {"accountId": "66b2e19d8c3f5a7e9d0b1c2e"}],
    from_="2026-11-16T00:00:00Z",
    to="2026-11-20T00:00:00Z",
    duration_minutes=30,
    default_open_hours=[{"days": [1, 2, 3, 4, 5], "timezone": "Europe/Madrid", "start": "09:00", "end": "17:00"}],
)

print(slots["data"]["timeSlots"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/calendar/availability" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "participants": [{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d" }, { "accountId": "66b2e19d8c3f5a7e9d0b1c2e" }],
    "from": "2026-11-16T00:00:00Z",
    "to": "2026-11-20T00:00:00Z",
    "durationMinutes": 30,
    "defaultOpenHours": [{ "days": [1, 2, 3, 4, 5], "timezone": "Europe/Madrid", "start": "09:00", "end": "17:00" }]
  }'
```
</Tab>
</Tabs>

```json
{
  "data": {
    "timeSlots": [
      { "from": "2026-11-16T08:00:00.000Z", "to": "2026-11-16T08:30:00.000Z", "accountIds": ["66b2e19d8c3f5a7e9d0b1c2d", "66b2e19d8c3f5a7e9d0b1c2e"] }
    ]
  }
}
```

`buffer` pads existing meetings, and `method: "max_availability"` returns slots where at least one host is free. See [Availability](/calendar/calendar-availability).

### Book it with a video link

Create the event on the host's calendar. The participants get an invitation, and `conferencing` attaches a Meet link (or `teams` on a Microsoft account).

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: event } = await zernio.calendar.createCalendarEvent({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    title: 'Demo call',
    start: '2026-11-16T09:00:00+01:00',
    end: '2026-11-16T09:30:00+01:00',
    participants: [{ email: 'client@example.com', name: 'Client' }],
    conferencing: 'google_meet',
  },
});

console.log(event.data.conferencing?.url);
```
</Tab>
<Tab value="Python">
```python
event = client.calendar.create_calendar_event(
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    title="Demo call",
    start="2026-11-16T09:00:00+01:00",
    end="2026-11-16T09:30:00+01:00",
    participants=[{"email": "client@example.com", "name": "Client"}],
    conferencing="google_meet",
)

print(event["data"]["conferencing"]["url"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/calendar/events" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "title": "Demo call",
    "start": "2026-11-16T09:00:00+01:00",
    "end": "2026-11-16T09:30:00+01:00",
    "participants": [{ "email": "client@example.com", "name": "Client" }],
    "conferencing": "google_meet"
  }'
```
</Tab>
</Tabs>

The response (`201`) is the created event, with the meeting link in `conferencing.url`. A Microsoft tenant without Teams returns `conferencing: null`.

### Answer an invitation

Set the user's response to an event they were invited to. `status` is `accepted`, `declined` or `tentative`.

```bash
curl -X POST "https://zernio.com/api/v1/calendar/events/6p1kq2rbc5i3eb9g/rsvp" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "status": "accepted", "comment": "See you there" }'
```

## How it behaves

- **Participants are notified.** Creating, changing or deleting an event emails the participants, as the calendar app does.
- **Times.** Send ISO 8601 with an offset, or a `YYYY-MM-DD` date with `allDay: true`.
- **Primary calendar.** Free-busy, availability and the `calendar.event.*` webhooks cover each account's primary calendar only.
- **Recurrence.** `recurrence` takes RRULE lines. Microsoft accepts a narrower set than Google. See [Create a calendar event](/calendar/create-calendar-event).

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `412` `account_scope_missing` | The account was connected without `calendar` | Connect it again with `calendar` in `scopes` |
| `400` on `conferencing` | `teams` on Google, or `google_meet` on Microsoft | Use the provider's own meeting type |
| `404` `resource_not_found` | The event is on another calendar | Pass its `calendarId` |

## Related

- [Google Calendar](/platforms/google/calendar)
- [Microsoft calendar](/platforms/microsoft/calendar)
- [Create a calendar event](/calendar/create-calendar-event)
- [Availability](/calendar/calendar-availability)
- [Calendar event created webhook](/webhooks/inbox/calendar-event-created)
- [Connected account pricing](/pricing/social)