Calendar
REST endpoints for the calendar: one window of events, viewings, open houses and expected closes, what is not done, busy time, and creating, moving, ticking and cancelling events.
Overview
The calendar is composed on the server for the person asking: their meetings, the viewings they host, the open houses they host or are going to, the day each of their deals is expected to close and, once they connect their Google or Microsoft account, their own appointments from that calendar.
The endpoints on this page use the dashboard's Clerk bearer token and resolve the organization from the request. Reading is open to every member. Writing needs an active plan, exactly as on the CRM endpoints, and only the person whose calendar an event is on can change it.
Invitations are never sent by Fondaro. When an event has attendees, it is created in the owner's own Google or Outlook calendar and their provider invites people, from their address, with them as the organiser. Without a connected calendar an event is the owner's own record, and a request that invites anyone is refused.
There is one kind of Fondaro entry: an event. A task was a lead, a title,
a due date and a tick; since 2026-09-26 each one is a calendar event with the
same id, and the tick is on the event (done). An event whose lead is invited
by nobody outside the team is owed until ticked. The CRM task endpoints
(/crm/tasks, integrations and MCP task tools) keep their routes and Task
shape, served from these same events.
Since 2026-09-29 a create or an edit can say which of the two the person meant
with kind (see Task or Event): a task is something you
owe a lead; an event is everything else, and a lead on it that is not
invited never makes it owed.
Endpoints
| Method & path | Who | Purpose |
|---|---|---|
GET /calendar | Any member | Everything in a window of dates |
GET /calendar/summary | Any member | How many owed events are overdue |
GET /calendar/overdue | Any member | The overdue events themselves, oldest first |
GET /calendar/busy | Any member | Your booked time, for conflict warnings |
GET /calendar/lead/:leadId/next | Anyone who can open the lead | The lead's overdue events and the next five |
GET /calendar/events/:id | Owner, or see below | One event in full |
POST /calendar/events | Any member | Create a meeting, or a viewing of your listing |
PATCH /calendar/events/:id | Owner | Move or edit an event |
DELETE /calendar/events/:id | Owner | Cancel an event |
POST /calendar/events/:id/complete | Owner, a teammate on it, or an admin | Tick it done |
DELETE /calendar/events/:id/complete | Owner, a teammate on it, or an admin | Untick it |
POST /calendar/events/:id/retry-sync | Owner | Try the calendar write again |
Task or Event
kind is a request field on create and edit; nothing is stored under that
name. The event itself says which it is: taskLike on
Read one event is true for a task.
"task": the first lead inleadsis the event's lead and is never invited (aninvite: trueon it is ignored). A task without a lead is400 CALENDAR_TASK_NEEDS_LEAD. It firestask.created."event": the first lead becomes the event's lead only when it is invited orpropertyListingIdmakes the event a viewing. Otherwise every lead rides on the event inattendees(leadId,invited: false): it shows on the lead's reads, but the event has noleadId, is never owed and firesmeeting.created.- Absent (phones, MCP, integrations): as before, the first lead is the event's lead.
On an edit, an absent kind keeps the event's own: a task keeps the old rules
and an event applies the event rule to any change of people. A kind that
differs from the event's own re-applies its rule to the people already on it,
even when no people field is sent.
Time zones and windows
from and to on GET /calendar are local dates (YYYY-MM-DD) in the zone
tz, an IANA name such as Europe/Madrid. to is exclusive, and a window is at
most 42 days, so a six-week month view fits in one call.
- Timed events, viewings and open houses are placed by their instant in
tz. - An all-day event comes back with
allDay: trueanddateset, and sits on that date whatever the zone. - An overdue event stays on its own day. Nothing is moved onto today; read
GET /calendar/overduefor the list.
Who sees what
- Expected closes are scoped as on
GET /crm/tasks: a member sees their own; an admin sees everyone's, or the people and teams named inassigneeIdsandteamIds. - Events are listed by whose calendar they are on: the owner's, and a
teammate's who is on the event (
attendees[].userId). A lead's reads (leadId=N,GET /calendar/lead/:leadId/next) take every event the lead is on: as its lead (leadId), as another lead or collaborator on it (attendees[].leadId), or as the collaborator of its viewing. An admin asking for a teammate withassigneeIdsgets that person's Fondaro meetings and viewings, never their own Google or Outlook appointments (kind: "external"). GET /calendar/events/:idreturns your own events, any event of a viewing, and, for an admin, a teammate's Fondaro events. Anything else is404.- Lead names follow the lead's own rule: an admin can open every lead, a
member the leads assigned to them and the shared collaborator leads. An
event on a lead you cannot open still shows, with its
leadId, butleadNameisnull(andlead.nameis""on the event in full).
The item object
GET /calendar returns one flat list, items, of a union by kind:
meeting, viewing, open_house (you host it), open_house_attending (you are
going), deal_close, external (your own Google or Outlook
appointment) and post (a listing post you scheduled or published to your own
Instagram or LinkedIn). Every item carries:
| Field | Type | Notes |
|---|---|---|
key | string | kind:id, stable across requests |
kind | string | See above |
startsAt | string or null | ISO-8601 instant |
endsAt | string or null | Exclusive; null for a point in time (a deal close, a post) |
allDay | boolean | |
date | string or null | YYYY-MM-DD for an all-day item, never shifted by the zone |
title | string | |
eventId, leadId, listingRef, openHouseId, dealId | What the item opens; null when it has none | |
leadName | string or null | null when the item has no lead, or its lead is one you cannot open |
sync | string | Your events only: none, pending, synced or failed |
ownerUserId | string or null | Clerk user id of whose calendar it is |
readOnly | boolean | Other people's items, your own appointments, repeating and cancelled events |
Per kind: a meeting (every event made in Fondaro) adds status, location,
attendeeCount (everyone on it but the owner: teammates, leads and guests),
invitedCount (how many invited people the provider tells about a move),
leadResponse (accepted, declined, tentative,
needs_action or null), done (ticked), owed (has a lead, invites nobody
outside the team, not cancelled, not done) and overdue (owed, and its day or
its start has passed);
a viewing adds status and viewingId; an open house adds status and
address; a deal_close adds status and stage; an external item adds status, location and
recurring; a post adds status (scheduled, publishing or
published), postId, channel (instagram or linkedin), postKind
(feed or story) and providerPostUrl. A post is a point in time
(endsAt is null), always readOnly, and opens with postId (see
Listing Posts); drafts are not on the calendar.
The list is lean on purpose: no descriptions and no attendee lists. Read one event for those.
Read a window
curl 'https://api.fondaro.com/calendar?from=2026-09-21&to=2026-09-28&tz=Europe/Madrid' \
-H 'Authorization: Bearer <clerk-session-token>'{
"window": { "from": "2026-09-21", "to": "2026-09-28", "tz": "Europe/Madrid" },
"items": [
{
"key": "meeting:6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
"kind": "meeting",
"startsAt": "2026-09-24T09:00:00.000Z",
"endsAt": "2026-09-24T09:30:00.000Z",
"allDay": false,
"date": null,
"title": "Meeting with a buyer",
"eventId": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
"leadId": 4182,
"leadName": "A. Buyer",
"listingRef": null,
"openHouseId": null,
"dealId": null,
"sync": "synced",
"ownerUserId": "user_2a",
"readOnly": false,
"status": "confirmed",
"location": "Office",
"attendeeCount": 1,
"invitedCount": 1,
"leadResponse": "accepted",
"done": false,
"owed": false,
"overdue": false
}
],
"truncated": false
}An admin reading a teammate's week adds &assigneeIds=user_2b (repeat the
parameter for several people) or &teamIds=<team uuid>.
&leadId=4182 reads one lead's window instead: every Fondaro event the lead is
on and the lead's viewings, whoever's calendar they are on, for anyone who can
open the lead (see Who sees what). assigneeIds and
teamIds are then ignored, and your own Google or Outlook appointments are
left out. A lead you cannot open is 404.
truncated is true only if one source hit its safety bound; a person's
calendar never does.
Count what is not done
curl 'https://api.fondaro.com/calendar/summary?tz=Europe/Madrid' \
-H 'Authorization: Bearer <clerk-session-token>'{ "overdueCount": 3 }How many owed events are overdue, counted in the database, so it is right
however many there are. assigneeIds and teamIds scope it as on
GET /calendar.
List what is not done
curl 'https://api.fondaro.com/calendar/overdue?tz=Europe/Madrid' \
-H 'Authorization: Bearer <clerk-session-token>'{
"items": [
{
"eventId": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
"title": "Call about the offer",
"startsAt": "2026-09-21T00:00:00.000Z",
"endsAt": "2026-09-22T00:00:00.000Z",
"allDay": true,
"date": "2026-09-21",
"leadId": 4182,
"leadName": "A. Buyer",
"ownerUserId": "user_2a"
}
],
"truncated": false
}Oldest first, at most 200; truncated is true when more were overdue.
leadId=N reads one lead's overdue events instead, for anyone who can open
the lead (assigneeIds and teamIds are then ignored; a lead you cannot open
is 404).
Read your busy time
from and to are ISO-8601 instants, at most 42 days apart. The answer holds
the events you own or are a teammate on that are neither done nor cancelled,
including your own Google or Outlook appointments and viewings you host. An
all-day event is one block over its whole day (allDay: true). Pass the event
you are editing as excludeEventId so it does not clash with itself.
curl 'https://api.fondaro.com/calendar/busy?from=2026-09-24T09:00:00Z&to=2026-09-24T10:00:00Z' \
-H 'Authorization: Bearer <clerk-session-token>'{
"busy": [
{
"key": "external:8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f",
"kind": "external",
"startsAt": "2026-09-24T09:15:00.000Z",
"endsAt": "2026-09-24T10:00:00.000Z",
"allDay": false,
"title": "Dentist",
"eventId": "8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f"
}
]
}Busy time is read from what Fondaro holds of your calendar; no request goes to Google or Microsoft.
Read a lead's next events
The lead page's Upcoming card: no window, so nothing far ahead is missed.
tz is required and judges "overdue" and "today".
curl 'https://api.fondaro.com/calendar/lead/4182/next?tz=Europe/Madrid' \
-H 'Authorization: Bearer <clerk-session-token>'{
"overdue": [
{
"key": "event:0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
"eventId": "0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f",
"kind": "meeting",
"title": "Call about the offer",
"startsAt": "2026-09-21T08:00:00.000Z",
"endsAt": "2026-09-21T08:15:00.000Z",
"allDay": false,
"date": null,
"status": "confirmed",
"ownerUserId": "user_2a",
"leadId": 4182,
"leadName": "A. Buyer",
"viewingId": null,
"attendeeCount": 1,
"done": false,
"owed": true,
"overdue": true,
"canComplete": true
}
],
"next": [],
"hasMore": false
}overdue lists the lead's overdue events, oldest first, at most 200. next
is the next five that are neither done nor cancelled, at any distance ahead
(an event under way counts); hasMore is true when more follow. The lead may
be on an event as its lead, as another lead or collaborator on it, or as the
collaborator of its viewing; leadId is the event's own lead. canComplete
is whether you may tick it (the owner, a teammate on it, or an admin). A lead
you cannot open is 404.
Read one event
:id is an event id, or viewing:<viewing id> for a viewing booked before the
calendar existed. Such a viewing has no event yet and is shown from the viewing
itself.
tz (optional) is your IANA zone. With it, overdue on an all-day event is
judged by your day, as GET /calendar does; without it, by the event's own
zone. A zone that is not IANA is 400 CALENDAR_TZ_INVALID.
curl 'https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b?tz=Europe/Madrid' \
-H 'Authorization: Bearer <clerk-session-token>'{
"id": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
"eventId": "6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b",
"kind": "meeting",
"origin": "fondaro",
"title": "Meeting with a buyer",
"description": "Bring the floor plans.",
"location": "Office",
"startsAt": "2026-09-24T09:00:00.000Z",
"endsAt": "2026-09-24T09:30:00.000Z",
"allDay": false,
"timezone": "Europe/Madrid",
"status": "confirmed",
"recurring": false,
"ownerUserId": "user_2a",
"attendees": [
{ "email": "buyer@example.com", "name": "A. Buyer", "leadId": 4182, "invited": true, "response": "accepted" },
{ "userId": "user_2b", "email": "ana@agency.example", "name": "Ana Ruiz", "response": "accepted" }
],
"people": [
{ "kind": "lead", "leadId": 4182, "name": "A. Buyer", "email": "buyer@example.com", "invited": true, "response": "accepted" },
{ "kind": "teammate", "userId": "user_2b", "name": "Ana Ruiz", "email": "ana@agency.example", "invited": false, "response": "accepted" }
],
"attendeeCount": 2,
"sync": "synced",
"syncError": null,
"provider": "google",
"lead": { "id": 4182, "name": "A. Buyer" },
"listingRef": null,
"viewing": null,
"openHouseId": null,
"history": null,
"canEdit": true,
"completedAt": null,
"owed": true,
"overdue": false,
"canComplete": true,
"taskLike": false,
"videoMissing": false
}syncispendinguntil your calendar holds the event, andfailedwhen the last write did not land (syncErrorsays why). A viewing or open house you deleted in Google or Outlook stays booked in Fondaro and readsfailedwithsyncError: "removed"until you add it back with retry.historyis set when an edit lost to a newer one made on the other side:{ "at", "side": "fondaro" | "external", "summary": "moved" | "edited" }. See When both sides change an event.providernames where the event lives. Fondaro holds no direct link to the event there; the dashboard opens that day in Google Calendar or Outlook.leadis{ "id", "name" }; for a lead you cannot open,nameis"". Send the sameleadIdback on an edit and the event keeps its lead.peopleis everyone on the event but the owner, the event's lead first:{ "kind": "teammate" | "lead" | "guest", "userId"?, "leadId"?, "name", "email"?, "invited", "response" }. A lead you cannot open hasname: ""and no email.attendeeCountispeople.length.taskLikeistruefor a task: a lead, nobody invited outside the team, not cancelled, done or not. Clients use it to show a tick and to open the event as a task.videoMissingistrue, for the owner only, when the event asked for a video call and the provider sent back no link. It clears oncelocationis set, for example to a pasted link.- In
attendees,invitedsays whether the person was sent an invite from your calendar. A lead can be on an event without one (invited: false). Entries written before 2026-09-26 have noinvited: an email and nouserIdmeans invited.
Create an event
curl -X POST 'https://api.fondaro.com/calendar/events' \
-H 'Authorization: Bearer <clerk-session-token>' \
-H 'Content-Type: application/json' \
-d '{
"title": "Meeting with a buyer",
"startsAt": "2026-09-24T09:00:00.000Z",
"endsAt": "2026-09-24T09:30:00.000Z",
"timezone": "Europe/Madrid",
"leads": [{ "leadId": 4182, "invite": true }, { "leadId": 4190 }],
"teammateIds": ["user_2b"],
"attendees": [{ "email": "colleague@example.com" }],
"location": "Office",
"videoCall": false,
"kind": "event",
"connectedAccountId": "9d2c4b1a-3e5f-4a6b-8c7d-0e1f2a3b4c5d",
"calendarId": "viewings@group.calendar.google.com"
}'A task needs only its lead, a title and a moment:
curl -X POST 'https://api.fondaro.com/calendar/events' \
-H 'Authorization: Bearer <clerk-session-token>' \
-H 'Content-Type: application/json' \
-d '{
"kind": "task",
"title": "Call A. Buyer",
"startsAt": "2026-09-25T10:00:00.000Z",
"endsAt": "2026-09-25T10:15:00.000Z",
"timezone": "Europe/Madrid",
"leads": [{ "leadId": 4182 }]
}'| Field | Required | Notes |
|---|---|---|
title | Yes | 1 to 500 characters |
startsAt, endsAt | Yes | ISO-8601 instants |
timezone | Yes | IANA zone the event is scheduled in |
allDay | No | |
leads | No | Up to 20 { leadId, invite? }, leads or collaborators you can see. The first is the event's lead. invite: true puts that lead's email on the invite (it needs an email); without it the lead is on the event but not invited |
leadId | No | Shorthand for one lead, leads: [{ leadId }]; ignored when leads is sent |
inviteLead | No | With leadId: invite that lead |
teammateIds | No | Up to 50 Clerk user ids of people in your organization. The event is on their calendars too and they can tick it; they are never sent an invite. Each gets access to every lead on it |
ownerUserId | No | Whose calendar it goes on: you by default. An admin may name one of teammateIds |
attendees | No | Up to 50 { email, name? }, always invited |
location, description | No | |
propertyListingId | No | One of your organization's listings: the event becomes a viewing of it, created through the viewing rules |
videoCall | No | Ask the owner's own calendar for a video link: Google Meet on a Google account, Microsoft Teams on a Microsoft one; there is no other video provider. Off unless true; ignored for a viewing. When location is empty, the link is put there. When the provider sends back no link, the event reads videoMissing: true |
kind | No | "task" or "event"; see Task or Event |
connectedAccountId | No | Which of the owner's calendar accounts it goes to: their mailbox, or a shared mailbox they connected, live and with calendar. Absent: the default account, the owner's oldest connected mailbox with calendar. The accounts and their calendars are on GET /connected-accounts |
calendarId | No | A calendar of that account (calendars[].id) that is not readOnly. Absent: the account's primary calendar. Sent without connectedAccountId, it is looked for on the default account |
The answer is the event, as Read one event returns it. With a
connected calendar the event is written there straight away and the provider
sends the invitations; sync shows how that went. A target that is not one of
the owner's writable calendars is 400 CALENDAR_TARGET_INVALID. A viewing
(propertyListingId) ignores the target and goes to the default calendar. An
edit never moves an event to another calendar.
Move or edit an event
Send only what changes. Moving keeps the length unless you send endsAt.
Attendees who stay keep their answers. kind turns a task into an event or
back (see Task or Event). leads, teammateIds and
attendees each replace their own group only when sent; leadId and inviteLead change
the event's lead only and keep the other leads. A change made in Google or
Outlook keeps every lead and teammate on the event: a lead whose invite was
removed there stays on the event, not invited.
curl -X PATCH 'https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b' \
-H 'Authorization: Bearer <clerk-session-token>' \
-H 'Content-Type: application/json' \
-d '{ "startsAt": "2026-09-24T11:00:00.000Z" }'- A viewing's event moves the viewing itself, through the same rules as the viewing endpoints.
:idmay beviewing:<viewing id>for a viewing that has no event yet: its event is created first, then the change applies. The answer carries the new event id.- An event with no calendar joins yours once you have connected one.
- Open houses change where they live:
400 CALENDAR_EVENT_OPEN_HOUSE.
Cancel an event
curl -X DELETE 'https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b' \
-H 'Authorization: Bearer <clerk-session-token>'A meeting becomes cancelled and is removed from your calendar, whose provider
tells the attendees. A viewing is cancelled as a viewing (viewing:<viewing id>
works here too). An open house is cancelled from the open house.
Tick an event done
curl -X POST 'https://api.fondaro.com/calendar/events/0f9c3a1e-2b4d-4c6e-8f10-2a3b4c5d6e7f/complete' \
-H 'Authorization: Bearer <clerk-session-token>'DELETE on the same path unticks it. Both are idempotent and answer the event,
as Read one event returns it, with completedAt set or
cleared. The owner, a teammate on the event, or an admin may tick it
(canComplete). A done event stays on its day. Ticking is a Fondaro mark: it
changes nothing in Google or Outlook. Your own appointments cannot be ticked
(400 CALENDAR_EVENT_READ_ONLY), nor can a viewing (400 CALENDAR_EVENT_VIEWING, mark it on the viewing), an open house (400 CALENDAR_EVENT_OPEN_HOUSE) or a cancelled event (409 CALENDAR_EVENT_CANCELLED). A tick fires the task.completed webhook when the
event was owed.
The CRM task endpoints and the MCP update_task / delete_task tools follow
the same rule: only the owner, a teammate on the event, or an admin can tick,
untick or delete a task (403 otherwise). Access to the lead alone lets you
read and edit the task, not tick or delete it.
Webhooks
Every event made in Fondaro fires one "created" webhook, never two:
- An owed event (a lead, nobody invited outside the team) fires
task.created, whether it is made on the calendar or through the task endpoints. - Any other event (one that invites someone, a viewing, an event with no
lead) fires
meeting.created, whoseleadIdslists every lead on it, the event's lead first.
An edit can make one new: kind: "task" on an event with a lead that
invites nobody outside the team makes it owed and fires task.created.
Inviting someone onto an owed event fires meeting.created. An edit without
kind keeps the event's own kind, so removing the last invitee from an event,
or adding a lead to it without inviting them, leaves it an event and fires
nothing. Other edits fire neither. Tasks created in bulk
(POST /crm/leads/bulk/tasks and the bulk_create_tasks tool) fire no
webhooks. See
Integrations for the payloads.
Try the calendar write again
curl -X POST 'https://api.fondaro.com/calendar/events/6b1f0c2e-7d3a-4e5f-9a8b-1c2d3e4f5a6b/retry-sync' \
-H 'Authorization: Bearer <clerk-session-token>'Writes the event to your calendar again (or removes it there, if cancelled). It
also adds back a viewing or open house you deleted in Google or Outlook. Fondaro
retries failed writes on its own every 15 minutes as well. Without a connected
calendar the answer is 409 CALENDAR_NOT_CONNECTED.
Errors
| 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_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 who is not an active member of your organization |
| 400 | CALENDAR_OWNER_NOT_ON_EVENT | ownerUserId is not one of teammateIds |
| 403 | CALENDAR_OWNER_NOT_ALLOWED | A member naming someone else in ownerUserId |
| 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 itself |
| 403 | CALENDAR_EVENT_NOT_YOURS | Someone else's event |
| 404 | CALENDAR_EVENT_NOT_FOUND | No such event, or not one you may see |
| 409 | CALENDAR_NOT_CONNECTED | Attendees, 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 |
Related Articles
Calendar guide
Everything you plan in one calendar, laid out like Google Calendar: tasks for your leads, meetings, viewings and open houses, with invites and video calls from your own Google or Outlook.
Connected inboxes and their calendars
REST endpoints to list, connect, reconnect and disconnect the inboxes your organization's members connect to Fondaro, choose which of an inbox's calendars show, read the price of the next one, and ask an admin for a paid one.
Property activity
Register and manage own-listing viewings, name an agent from another agency on them, and read the merged property activity feed.
API Overview
Introduction to the Fondaro API: base URL, authentication, scopes, OpenAPI, response format, errors, pagination and rate limits.