# Calendar

List calendars and events, create events with Google Meet or Teams links, answer invitations and find free meeting slots across Google and Microsoft mailboxes.

Work in your users' own Google Calendar or Outlook Calendar. Create a booking with a video link, answer an invitation, or find a time that suits several people across Google and Microsoft accounts at once. Every call takes the `accountId` of an account connected with the `calendar` area.

## List calendars and events

`GET /v1/calendar/calendars?accountId=...` returns the user's calendars with `primary`, `timezone` and `readOnly`. Use an `id` from it as `calendarId`, or `primary` for the main calendar.

`GET /v1/calendar/events` returns events between `from` and `to` (both required):

```bash cURL
curl "https://api.pozy.ai/v1/calendar/events?accountId=6710c501e4b0d2f1a8c95e77&from=2026-11-02T00:00:00Z&to=2026-11-09T00:00:00Z" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/calendar/events?accountId=6710c501e4b0d2f1a8c95e77&from=2026-11-02T00:00:00Z&to=2026-11-09T00:00:00Z", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const e of data.data) console.log(e.start, e.title, e.conferencing?.url);
```
```python Python
import os, requests

events = requests.get(
    "https://api.pozy.ai/v1/calendar/events",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"accountId": "6710c501e4b0d2f1a8c95e77", "from": "2026-11-02T00:00:00Z", "to": "2026-11-09T00:00:00Z"},
).json()["data"]
```

| Parameter | What it does |
| --- | --- |
| `calendarId` | Which calendar; `primary` when left out |
| `expandRecurring` | With `true`, the default, a repeating event comes back once per occurrence. With `false` you get the series itself |
| `showCancelled` | `true` adds cancelled events; they are hidden by default |
| `title`, `location`, `icalUid` | Filter on these |
| `timezone` | Times in the response are given in this IANA zone |
| `sortOrder`, `cursor`, `limit` | Ascending unless you say otherwise; 50 events a page by default, 200 at most |

Each event has `title`, `start`, `end`, `allDay`, `participants` (with their response `status`), `conferencing` (`provider` and `url`), `status` and a link to open it in the provider's app.

## Create an event

Participants get invitations from the user's own calendar. Set `conferencing` to `google_meet` on Google or `teams` on Microsoft to attach a video link.

```bash cURL
curl -X POST https://api.pozy.ai/v1/calendar/events \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c501e4b0d2f1a8c95e77",
    "title": "Content review with Jordan",
    "start": "2026-11-05T10:00:00+01:00",
    "end": "2026-11-05T10:30:00+01:00",
    "timezone": "Europe/Madrid",
    "participants": [{ "email": "jordan@client.example", "name": "Jordan Lee" }],
    "conferencing": "google_meet",
    "reminders": { "useDefault": false, "overrides": [{ "minutes": 15 }] },
    "metadata": { "bookingId": "bk_9081" }
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/calendar/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6710c501e4b0d2f1a8c95e77",
    title: "Content review with Jordan",
    start: "2026-11-05T10:00:00+01:00",
    end: "2026-11-05T10:30:00+01:00",
    participants: [{ email: "jordan@client.example", name: "Jordan Lee" }],
    conferencing: "google_meet",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.data.id, data.data.conferencing?.url);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/calendar/events",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6710c501e4b0d2f1a8c95e77",
        "title": "Content review with Jordan",
        "start": "2026-11-05T10:00:00+01:00",
        "end": "2026-11-05T10:30:00+01:00",
        "participants": [{"email": "jordan@client.example"}],
        "conferencing": "google_meet",
    },
)
```

| Field | Rules |
| --- | --- |
| `calendarId` | The calendar that gets the event; `primary` if omitted |
| `title` | Required; 1,000 characters at most |
| `start`, `end` | Date-times in ISO 8601 with an offset. For an all-day event, plain `YYYY-MM-DD` dates plus `allDay: true` |
| `timezone` | The event's IANA time zone |
| `participants` | A list of `{ email, name? }`, 100 at most |
| `recurrence` | On Google, as many as 5 lines of RRULE, EXRULE, RDATE or EXDATE. On Outlook, a single RRULE whose frequency is `DAILY`, `WEEKLY`, `MONTHLY` or `YEARLY` |
| `reminders` | `{ useDefault, overrides }`. Google accepts 5 overrides, each between 0 and 40,320 minutes; Outlook uses only the first one |
| `conferencing` | `google_meet` on Google, `teams` on Microsoft. Asking a provider for the other one's meetings gets `400` |
| `description`, `location`, `busy`, `visibility` | `visibility` takes `default`, `public` or `private` |
| `metadata` | Your own string keys and values, stored with the event |

`PATCH /v1/calendar/events/{eventId}` changes an event and takes the same fields. `DELETE /v1/calendar/events/{eventId}?accountId=...` removes it. Participants get the usual invitation, update or cancellation email for each, exactly as if the user had made the change in the calendar app.

> **Events on other calendars:**
> 
> Reading, changing, deleting and answering an event all look in the primary calendar by default. For an event elsewhere, add `calendarId`: as a query parameter on `GET` and `DELETE`, and in the body on `PATCH` and RSVP. If you get `404 resource_not_found`, the event is most likely on a different calendar.

## Answer an invitation

```bash cURL
curl -X POST https://api.pozy.ai/v1/calendar/events/6p1kq2rbc5i3eb9g/rsvp \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "6710c501e4b0d2f1a8c95e77", "status": "accepted", "comment": "See you there" }'
```

`status` is `accepted`, `declined` or `tentative`. `comment` (up to 1,000 characters) goes to the organizer. The mailbox must be an invited attendee.

## Find a meeting time

`POST /v1/calendar/free-busy` returns busy blocks for up to 10 mailboxes, Google and Microsoft mixed. `POST /v1/calendar/availability` turns them into bookable slots:

```bash cURL
curl -X POST https://api.pozy.ai/v1/calendar/availability \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "participants": [{ "accountId": "6710c501e4b0d2f1a8c95e77" }, { "accountId": "6710c50ce4b0d2f1a8c95e82" }],
    "from": "2026-11-02T00:00:00Z",
    "to": "2026-11-07T00:00:00Z",
    "durationMinutes": 30,
    "buffer": { "before": 10, "after": 10 },
    "defaultOpenHours": [{ "days": [1, 2, 3, 4, 5], "timezone": "Europe/Madrid", "start": "09:00", "end": "17:00" }]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/calendar/availability", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    participants: [{ accountId: "6710c501e4b0d2f1a8c95e77" }, { accountId: "6710c50ce4b0d2f1a8c95e82" }],
    from: "2026-11-02T00:00:00Z",
    to: "2026-11-07T00:00:00Z",
    durationMinutes: 30,
    defaultOpenHours: [{ days: [1, 2, 3, 4, 5], timezone: "Europe/Madrid", start: "09:00", end: "17:00" }],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.data.timeSlots.slice(0, 3));
```
```python Python
slots = requests.post(
    "https://api.pozy.ai/v1/calendar/availability",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "participants": [{"accountId": "6710c501e4b0d2f1a8c95e77"}, {"accountId": "6710c50ce4b0d2f1a8c95e82"}],
        "from": "2026-11-02T00:00:00Z",
        "to": "2026-11-07T00:00:00Z",
        "durationMinutes": 30,
    },
).json()["data"]["timeSlots"]
```

| Field | Rules |
| --- | --- |
| `participants` | Between 1 and 10 entries of `{ accountId, openHours? }` |
| `durationMinutes` | How long each slot is, from 5 to 480 |
| `intervalMinutes` | Minutes from one slot's start to the next, from 5 to 240; 30 by default |
| `roundTo` | Moves `from` forward to the next multiple of this many minutes (5 to 60) |
| `buffer` | Free minutes to keep `{ before, after }` other meetings, 240 at most on each side |
| `defaultOpenHours`, `openHours` | When people work: `days` (Sunday is 0), `timezone`, `start` and `end` in `HH:MM`, and `exdates` to skip |
| `method` | `collective`, the default, needs every participant free. `max_availability` needs one, and `accountIds` says which |

Both the free-busy and availability calls use the primary calendar of each mailbox. Outlook's data comes in 15-minute blocks.

## Webhook events

Any change to the primary calendar fires `calendar.event.created`, `calendar.event.updated` or `calendar.event.deleted`, no matter whether it came through Pozy or from the user's own calendar app. The payload has `account`, plus `calendarEvent` with the event's `id` and `changeType`; read the event to see how it looks now. A provider may report one change more than once, so deduplicate on the event id.

| Event | When it fires |
| --- | --- |
| [`calendar.event.created`](https://pozy.ai/docs/webhooks/events#calendar-event-created) | Calendar event created. |
| [`calendar.event.updated`](https://pozy.ai/docs/webhooks/events#calendar-event-updated) | Calendar event updated. |
| [`calendar.event.deleted`](https://pozy.ai/docs/webhooks/events#calendar-event-deleted) | Calendar event deleted. |

## Errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400 invalid_field_value`, `param: conferencing` | `teams` on Google or `google_meet` on Microsoft | Use the provider's own option |
| `400` "not an attendee" | RSVP from a mailbox that was not invited | RSVP from the invited mailbox |
| `404 resource_not_found` | Wrong calendar, or the event was deleted | Pass the event's `calendarId` |
| `412 account_scope_missing` | No `calendar` area on the account | Reconnect with `calendar` in `scopes` |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/calendar/calendars`](https://pozy.ai/docs/api/listCalendars) | List calendars |
| [`GET /v1/calendar/events`](https://pozy.ai/docs/api/listCalendarEvents) | List events in a window |
| [`POST /v1/calendar/events`](https://pozy.ai/docs/api/createCalendarEvent) | Create an event |
| [`GET /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/getCalendarEvent) | Get an event |
| [`PATCH /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/updateCalendarEvent) | Update an event |
| [`DELETE /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/deleteCalendarEvent) | Delete an event |
| [`POST /v1/calendar/events/{eventId}/rsvp`](https://pozy.ai/docs/api/rsvpCalendarEvent) | Respond to an invitation |
| [`POST /v1/calendar/free-busy`](https://pozy.ai/docs/api/calendarFreeBusy) | Busy blocks for one or more accounts |
| [`POST /v1/calendar/availability`](https://pozy.ai/docs/api/calendarAvailability) | Find meeting slots across accounts |
