# Calendar

> Read a person's calendar window, overdue tasks and busy time, and create, move, tick and cancel tasks and events.

The Calendar API returns one person's calendar (their tasks and events, the viewings and open houses they host or attend, their deals' expected closes and their own Google or Outlook appointments) and creates and changes Fondaro events.

These endpoints take a signed-in member's session token (`Authorization: Bearer <session-token>`); API keys do not open them. See [Authentication](https://www.fondaro.com/docs/api/authentication.md).

Reading is open to every member. Writes need an active plan (`403 SUBSCRIPTION_REQUIRED` otherwise), and only the owner of an event can change it. Fondaro never sends invitations itself: an event with invited people is written to the owner's own Google or Outlook calendar, and that provider invites them from the owner's address.

## Tasks and events

Every Fondaro entry is an **event**. A **task** is an event with a lead and nobody invited from outside the team: it is owed until it is ticked done, and overdue once its time has passed. Anything else (a meeting with invited people, a viewing, an event with no lead) is never owed.

Create and edit requests may send `kind`:

| `kind` | Effect |
|---|---|
| `"task"` | The first entry in `leads` is the task's lead and is never invited. A task without a lead is `400 CALENDAR_TASK_NEEDS_LEAD`. |
| `"event"` | The first lead becomes the event's lead only when invited, or when `propertyListingId` makes it a viewing. Other leads ride along as attendees with `invited: false`, and the event is never owed. |
| absent | The first lead is the event's lead. On an edit, the event keeps its own kind. |

The event reports which it is in `taskLike` (see [Read one event](#read-one-event)).

## Read a window

`GET /calendar`

Everything on the caller's calendar between two local dates, as one flat list.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `from` | Query | `YYYY-MM-DD` | Yes | First local date |
| `to` | Query | `YYYY-MM-DD` | Yes | Exclusive end date; at most 42 days after `from` |
| `tz` | Query | string | Yes | IANA zone, such as `Europe/Madrid` |
| `assigneeIds` | Query | string[] | No | Admins: read these people's calendars (repeat the parameter, up to 50) |
| `teamIds` | Query | uuid[] | No | Admins: read these teams' calendars (up to 20) |
| `leadId` | Query | integer | No | Read one lead's window instead: every Fondaro event the lead is on and its viewings, whoever owns them. `assigneeIds` and `teamIds` are ignored |

```bash
curl "https://api.fondaro.com/calendar?from=2026-10-05&to=2026-10-12&tz=Europe/Madrid" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "window": { "from": "2026-10-05", "to": "2026-10-12", "tz": "Europe/Madrid" },
  "items": [
    {
      "key": "meeting:6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
      "kind": "meeting",
      "startsAt": "2026-10-07T09:00:00.000Z",
      "endsAt": "2026-10-07T09:30:00.000Z",
      "allDay": false,
      "date": null,
      "title": "Meeting with a buyer",
      "eventId": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
      "leadId": 4182,
      "leadName": "A. Buyer",
      "sync": "synced",
      "ownerUserId": "user_2a",
      "readOnly": false,
      "status": "confirmed",
      "attendeeCount": 1,
      "leadResponse": "accepted",
      "done": false,
      "owed": false,
      "overdue": false
    }
  ],
  "truncated": false
}
```

Timed items are placed by their instant in `tz`; an all-day item has `allDay: true` and a `date` that never shifts with the zone. An overdue task stays on its own day. `truncated` is `true` only when a source hit its safety bound.

### Item fields

| Field | Description |
|---|---|
| `key` | `kind:id`, stable across requests |
| `kind` | `meeting` (any Fondaro event), `viewing`, `open_house` (you host it), `open_house_attending`, `deal_close`, `external` (your own Google or Outlook appointment) or `post` (a listing post) |
| `startsAt`, `endsAt` | ISO 8601 instants; `endsAt` is exclusive and `null` for a point in time |
| `allDay`, `date` | `date` is `YYYY-MM-DD` for an all-day item |
| `eventId`, `leadId`, `listingRef`, `openHouseId`, `dealId` | What the item opens; `null` when it has none |
| `leadName` | `null` when there is no lead or you cannot open it |
| `sync` | Your events only: `none`, `pending`, `synced` or `failed` |
| `ownerUserId` | Whose calendar it is on |
| `readOnly` | `true` for other people's items, your own appointments, and repeating or cancelled events |

Per kind: a `meeting` adds `status`, `location`, `attendeeCount`, `invitedCount`, `leadResponse` (`accepted`, `declined`, `tentative`, `needs_action` or `null`), `done`, `owed` and `overdue`. A `viewing` adds `status` and `viewingId`; an open house `status` and `address`; a `deal_close` `status` and `stage`; an `external` item `status`, `location` and `recurring`; a `post` `status` (`scheduled`, `publishing` or `published`), `postId`, `channel`, `postKind` and `providerPostUrl`.

### Who sees what

- Events are listed on the owner's calendar and on the calendar of every teammate on the event.
- Expected closes: a member sees their own; an admin sees everyone's, or those of `assigneeIds` and `teamIds`.
- An admin reading a teammate gets their Fondaro events and viewings, never their `external` appointments.
- An event on a lead you cannot open still shows, with `leadName: null`.

## Count overdue tasks

`GET /calendar/summary`

How many owed tasks are overdue.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `tz` | Query | string | Yes | IANA zone that decides "today" |
| `assigneeIds`, `teamIds` | Query | arrays | No | As on `GET /calendar` |

```bash
curl "https://api.fondaro.com/calendar/summary?tz=Europe/Madrid" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "overdueCount": 3 }
```

## List overdue tasks

`GET /calendar/overdue`

The overdue tasks themselves, oldest first, at most 200.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `tz` | Query | string | Yes | IANA zone |
| `assigneeIds`, `teamIds` | Query | arrays | No | As on `GET /calendar` |
| `leadId` | Query | integer | No | One lead's overdue tasks, for anyone who can open the lead |

```bash
curl "https://api.fondaro.com/calendar/overdue?tz=Europe/Madrid" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "items": [
    {
      "eventId": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
      "title": "Call about the offer",
      "startsAt": "2026-10-01T00:00:00.000Z",
      "endsAt": "2026-10-02T00:00:00.000Z",
      "allDay": true,
      "date": "2026-10-01",
      "leadId": 4182,
      "leadName": "A. Buyer",
      "ownerUserId": "user_2a"
    }
  ],
  "truncated": false
}
```

`truncated` is `true` when more than 200 were overdue.

## Read busy time

`GET /calendar/busy`

The caller's booked time for conflict warnings: events they own or are on that are neither done nor cancelled, their own appointments and the viewings they host. Read from what Fondaro holds; no request goes to Google or Microsoft.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `from`, `to` | Query | ISO 8601 instant | Yes | At most 42 days apart |
| `excludeEventId` | Query | uuid | No | The event being edited, so it does not clash with itself |

```bash
curl "https://api.fondaro.com/calendar/busy?from=2026-10-07T09:00:00Z&to=2026-10-07T10:00:00Z" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "busy": [
    {
      "key": "external:8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f",
      "kind": "external",
      "startsAt": "2026-10-07T09:15:00.000Z",
      "endsAt": "2026-10-07T10:00:00.000Z",
      "allDay": false,
      "title": "Dentist",
      "eventId": "8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f"
    }
  ]
}
```

## Read a lead's next events

`GET /calendar/lead/:leadId/next`

A lead's overdue tasks and its next five events, with no date window. The lead may be on an event as its lead, as another lead or collaborator, or as the collaborator of a viewing.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `leadId` | Path | integer | Yes | The lead |
| `tz` | Query | string | Yes | IANA zone that decides "overdue" and "today" |

```bash
curl "https://api.fondaro.com/calendar/lead/4182/next?tz=Europe/Madrid" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "overdue": [
    {
      "key": "event:0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
      "eventId": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
      "kind": "meeting",
      "title": "Call about the offer",
      "startsAt": "2026-10-01T08:00:00.000Z",
      "endsAt": "2026-10-01T08:15:00.000Z",
      "allDay": false,
      "status": "confirmed",
      "ownerUserId": "user_2a",
      "leadId": 4182,
      "leadName": "A. Buyer",
      "viewingId": null,
      "done": false,
      "owed": true,
      "overdue": true,
      "canComplete": true
    }
  ],
  "next": [],
  "hasMore": false
}
```

`overdue` holds at most 200, oldest first. `next` holds the next five events that are neither done nor cancelled, including one under way; `hasMore` says more follow. `canComplete` says whether you may tick it.

## Read one event

`GET /calendar/events/:id`

One event in full, with its people. You can read your own events, any viewing's event and, as an admin, a teammate's Fondaro events.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | Path | string | Yes | Event id, or `viewing:<viewingId>` for a viewing that has no event yet |
| `tz` | Query | string | No | IANA zone used to judge `overdue` on an all-day task |

```bash
curl "https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b?tz=Europe/Madrid" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "id": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
  "kind": "meeting",
  "title": "Meeting with a buyer",
  "description": "Bring the floor plans.",
  "location": "Office",
  "startsAt": "2026-10-07T09:00:00.000Z",
  "endsAt": "2026-10-07T09:30:00.000Z",
  "allDay": false,
  "timezone": "Europe/Madrid",
  "status": "confirmed",
  "ownerUserId": "user_2a",
  "people": [
    { "kind": "lead", "leadId": 4182, "name": "A. Buyer", "email": "buyer@example.com", "invited": true, "response": "accepted" },
    { "kind": "teammate", "userId": "user_2b", "name": "Ana Ruiz", "invited": false, "response": "accepted" }
  ],
  "attendeeCount": 2,
  "sync": "synced",
  "syncError": null,
  "provider": "google",
  "lead": { "id": 4182, "name": "A. Buyer" },
  "viewing": null,
  "canEdit": true,
  "completedAt": null,
  "owed": false,
  "overdue": false,
  "canComplete": true,
  "taskLike": false,
  "videoMissing": false
}
```

| Field | Description |
|---|---|
| `people` | Everyone on the event but the owner, the event's lead first. `kind` is `teammate`, `lead` or `guest`; `invited` says whether the provider invited them |
| `sync`, `syncError` | `pending` until your calendar holds the event, `failed` when the last write did not land. A viewing or open house deleted in Google or Outlook reads `failed` with `syncError: "removed"` |
| `provider` | Where the event lives (`google` or `microsoft`), or `null` |
| `history` | Set when an edit lost to a newer change on the other side: `{ at, side, summary }` |
| `taskLike` | `true` for a task |
| `videoMissing` | Owner only: a video call was asked for and the provider returned no link |

A lead you cannot open reads with an empty `name` and no email.

## Create an event

`POST /calendar/events`

Creates a task, an event, or a viewing of one of your listings. With a connected calendar the event is written there at once and the provider sends the invitations. Without one, an event that invites anyone is refused.

| Field | Type | Required | Description |
|---|---|---|---|
| `title` | string | Yes | 1 to 500 characters |
| `startsAt`, `endsAt` | ISO 8601 | Yes | `endsAt` must not be before `startsAt` |
| `timezone` | string | Yes | IANA zone the event is scheduled in |
| `allDay` | boolean | No | |
| `kind` | string | No | `task` or `event`, see [Tasks and events](#tasks-and-events) |
| `leads` | array | No | Up to 20 `{ leadId, invite? }`. The first is the event's lead. `invite: true` invites that lead (it needs an email) |
| `leadId`, `inviteLead` | integer, boolean | No | Shorthand for one lead; ignored when `leads` is sent |
| `teammateIds` | string[] | No | Up to 50 user ids of members. The event is on their calendars and they can tick it; they get no invite |
| `ownerUserId` | string | No | Whose calendar it goes on; defaults to you. An admin may name one of `teammateIds` |
| `attendees` | array | No | Up to 50 `{ email, name? }`, always invited |
| `location` | string | No | Up to 1,000 characters |
| `description` | string | No | Up to 8,000 characters |
| `propertyListingId` | uuid | No | One of your organization's listings: the event becomes a viewing |
| `videoCall` | boolean | No | Ask the owner's Google calendar for a Google Meet link. Put in `location` when it is empty. Ignored for a viewing |
| `connectedAccountId` | uuid | No | Which of the owner's connected accounts with a calendar to write to; defaults to their oldest one |
| `calendarId` | string | No | A writable calendar of that account; defaults to its primary calendar |

```bash
curl -X POST "https://api.fondaro.com/calendar/events" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "task",
    "title": "Call A. Buyer",
    "startsAt": "2026-10-08T10:00:00.000Z",
    "endsAt": "2026-10-08T10:15:00.000Z",
    "timezone": "Europe/Madrid",
    "leads": [{ "leadId": 4182 }]
  }'
```

```json
{
  "id": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
  "kind": "meeting",
  "title": "Call A. Buyer",
  "startsAt": "2026-10-08T10:00:00.000Z",
  "endsAt": "2026-10-08T10:15:00.000Z",
  "lead": { "id": 4182, "name": "A. Buyer" },
  "sync": "pending",
  "owed": true,
  "taskLike": true
}
```

The response is the event as [Read one event](#read-one-event) returns it. A viewing ignores `connectedAccountId` and `calendarId` and goes to the default calendar. The accounts and their calendars are listed by [Connected inboxes](https://www.fondaro.com/docs/api/connected-accounts.md).

## Move or edit an event

`PATCH /calendar/events/:id`

Send only what changes. Takes the create fields except `propertyListingId`, `ownerUserId`, `videoCall`, `connectedAccountId` and `calendarId`.

- Moving keeps the length unless you send `endsAt`. Attendees who stay keep their answers.
- `leads`, `teammateIds` and `attendees` each replace their own group only when sent. `leadId` and `inviteLead` change the event's lead and keep the other leads.
- A `kind` different from the event's own re-applies its rule to the people already on it.
- A viewing's event moves the viewing. `:id` may be `viewing:<viewingId>`; the event is created first and the response carries its new id.
- An edit never moves an event to another calendar. Open houses are changed from the open house (`400 CALENDAR_EVENT_OPEN_HOUSE`).

```bash
curl -X PATCH "https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "startsAt": "2026-10-07T11:00:00.000Z" }'
```

```json
{
  "id": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
  "startsAt": "2026-10-07T11:00:00.000Z",
  "endsAt": "2026-10-07T11:30:00.000Z",
  "sync": "pending"
}
```

## Cancel an event

`DELETE /calendar/events/:id`

A meeting becomes `cancelled` and is removed from the owner's calendar, whose provider tells the attendees. A viewing is cancelled as a viewing (`viewing:<viewingId>` works here too). Open houses are cancelled from the open house.

```bash
curl -X DELETE "https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "id": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b", "status": "cancelled" }
```

## Tick an event done

`POST /calendar/events/:id/complete` ticks it; `DELETE /calendar/events/:id/complete` unticks it.

Both are idempotent and return the event with `completedAt` set or cleared. The owner, a teammate on the event or an admin may tick it. A done task stays on its day, and ticking changes nothing in Google or Outlook. Your own appointments, viewings, open houses and cancelled events cannot be ticked.

```bash
curl -X POST "https://api.fondaro.com/calendar/events/0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f/complete" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "id": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f", "completedAt": "2026-10-08T10:20:00.000Z", "owed": false }
```

The CRM task endpoints and the MCP task tools follow the same rule: only the owner, a teammate on the event or an admin can tick, untick or delete a task.

## Retry a calendar write

`POST /calendar/events/:id/retry-sync`

Writes the event to the owner's calendar again, or removes it there if cancelled. It also restores a viewing or open house deleted in Google or Outlook. Fondaro retries failed writes on its own every 15 minutes.

```bash
curl -X POST "https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b/retry-sync" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "id": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b", "sync": "synced", "syncError": null }
```

## Webhooks

Every event made in Fondaro fires one created webhook:

- A task (a lead, nobody invited from outside the team) fires `task.created`.
- Any other event fires `meeting.created`, with `leadIds` listing every lead on it, the event's lead first.

An edit with `kind: "task"` that makes an event owed fires `task.created`; inviting someone onto a task fires `meeting.created`. Other edits fire nothing, and bulk task creation fires no webhooks. Ticking a task fires `task.completed`. See [Integration reporting](https://www.fondaro.com/docs/api/integrations.md) for payloads.

## Errors

Errors carry a stable `code`.

| Status | `code` | When |
|---|---|---|
| 400 | `CALENDAR_TZ_REQUIRED`, `CALENDAR_TZ_INVALID` | No zone, or not an IANA zone |
| 400 | `CALENDAR_WINDOW_INVALID`, `CALENDAR_WINDOW_TOO_WIDE` | Bad dates, or more than 42 days |
| 400 | `CALENDAR_EVENT_INVALID` | Bad instants, or `endsAt` before `startsAt` |
| 400 | `CALENDAR_LEAD_NO_EMAIL` | Inviting a lead with no email |
| 400 | `CALENDAR_TASK_NEEDS_LEAD` | `kind: "task"` with no lead |
| 400 | `CALENDAR_TARGET_INVALID` | `connectedAccountId` or `calendarId` is not one of the owner's writable calendars |
| 400 | `CALENDAR_TEAMMATE_UNKNOWN` | A `teammateIds` entry is not an active member |
| 400 | `CALENDAR_OWNER_NOT_ON_EVENT` | `ownerUserId` is not one of `teammateIds` |
| 400 | `CALENDAR_EVENT_READ_ONLY` | Your own Google or Outlook appointment, or a repeating event |
| 400 | `CALENDAR_EVENT_OPEN_HOUSE` | Change or cancel an open house from the open house |
| 400 | `CALENDAR_EVENT_VIEWING` | Mark a viewing done on the viewing |
| 403 | `CALENDAR_OWNER_NOT_ALLOWED` | A member naming someone else as `ownerUserId` |
| 403 | `CALENDAR_EVENT_NOT_YOURS` | Someone else's event |
| 403 | `SUBSCRIPTION_REQUIRED` | A write without an active plan |
| 404 | `CALENDAR_EVENT_NOT_FOUND` | No such event, or not one you may see |
| 404 | | `leadId` names a lead you cannot open |
| 409 | `CALENDAR_NOT_CONNECTED` | Invited people, or a retry, without a connected calendar |
| 409 | `CALENDAR_EVENT_CANCELLED` | The event is cancelled |
| 409 | `CALENDAR_VIEWING_NOT_MIRRORED` | The viewing was saved but its event was not; try again |

Limits: a window or busy range is at most 42 days; overdue lists return at most 200 items; an event holds up to 20 leads, 50 teammates and 50 invited guests.

Source: https://www.fondaro.com/docs/api/calendar
