Email, calendar & files
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):
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"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);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.
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" }
}'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);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.
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
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:
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" }]
}'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));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 |
Calendar event created. |
calendar.event.updated |
Calendar event updated. |
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 |
List calendars |
GET /v1/calendar/events |
List events in a window |
POST /v1/calendar/events |
Create an event |
GET /v1/calendar/events/{eventId} |
Get an event |
PATCH /v1/calendar/events/{eventId} |
Update an event |
DELETE /v1/calendar/events/{eventId} |
Delete an event |
POST /v1/calendar/events/{eventId}/rsvp |
Respond to an invitation |
POST /v1/calendar/free-busy |
Busy blocks for one or more accounts |
POST /v1/calendar/availability |
Find meeting slots across accounts |