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 & pathWhoPurpose
GET /calendarAny memberEverything in a window of dates
GET /calendar/summaryAny memberHow many owed events are overdue
GET /calendar/overdueAny memberThe overdue events themselves, oldest first
GET /calendar/busyAny memberYour booked time, for conflict warnings
GET /calendar/lead/:leadId/nextAnyone who can open the leadThe lead's overdue events and the next five
GET /calendar/events/:idOwner, or see belowOne event in full
POST /calendar/eventsAny memberCreate a meeting, or a viewing of your listing
PATCH /calendar/events/:idOwnerMove or edit an event
DELETE /calendar/events/:idOwnerCancel an event
POST /calendar/events/:id/completeOwner, a teammate on it, or an adminTick it done
DELETE /calendar/events/:id/completeOwner, a teammate on it, or an adminUntick it
POST /calendar/events/:id/retry-syncOwnerTry 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 in leads is the event's lead and is never invited (an invite: true on it is ignored). A task without a lead is 400 CALENDAR_TASK_NEEDS_LEAD. It fires task.created.
  • "event": the first lead becomes the event's lead only when it is invited or propertyListingId makes the event a viewing. Otherwise every lead rides on the event in attendees (leadId, invited: false): it shows on the lead's reads, but the event has no leadId, is never owed and fires meeting.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: true and date set, and sits on that date whatever the zone.
  • An overdue event stays on its own day. Nothing is moved onto today; read GET /calendar/overdue for 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 in assigneeIds and teamIds.
  • 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 with assigneeIds gets that person's Fondaro meetings and viewings, never their own Google or Outlook appointments (kind: "external").
  • GET /calendar/events/:id returns your own events, any event of a viewing, and, for an admin, a teammate's Fondaro events. Anything else is 404.
  • 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, but leadName is null (and lead.name is "" 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:

FieldTypeNotes
keystringkind:id, stable across requests
kindstringSee above
startsAtstring or nullISO-8601 instant
endsAtstring or nullExclusive; null for a point in time (a deal close, a post)
allDayboolean
datestring or nullYYYY-MM-DD for an all-day item, never shifted by the zone
titlestring
eventId, leadId, listingRef, openHouseId, dealIdWhat the item opens; null when it has none
leadNamestring or nullnull when the item has no lead, or its lead is one you cannot open
syncstringYour events only: none, pending, synced or failed
ownerUserIdstring or nullClerk user id of whose calendar it is
readOnlybooleanOther 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
}
  • sync is pending until your calendar holds the event, and failed when the last write did not land (syncError says why). A viewing or open house you deleted in Google or Outlook stays booked in Fondaro and reads failed with syncError: "removed" until you add it back with retry.
  • history is 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.
  • provider names where the event lives. Fondaro holds no direct link to the event there; the dashboard opens that day in Google Calendar or Outlook.
  • lead is { "id", "name" }; for a lead you cannot open, name is "". Send the same leadId back on an edit and the event keeps its lead.
  • people is 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 has name: "" and no email. attendeeCount is people.length.
  • taskLike is true for 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.
  • videoMissing is true, for the owner only, when the event asked for a video call and the provider sent back no link. It clears once location is set, for example to a pasted link.
  • In attendees, invited says 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 no invited: an email and no userId means 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 }]
  }'
FieldRequiredNotes
titleYes1 to 500 characters
startsAt, endsAtYesISO-8601 instants
timezoneYesIANA zone the event is scheduled in
allDayNo
leadsNoUp 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
leadIdNoShorthand for one lead, leads: [{ leadId }]; ignored when leads is sent
inviteLeadNoWith leadId: invite that lead
teammateIdsNoUp 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
ownerUserIdNoWhose calendar it goes on: you by default. An admin may name one of teammateIds
attendeesNoUp to 50 { email, name? }, always invited
location, descriptionNo
propertyListingIdNoOne of your organization's listings: the event becomes a viewing of it, created through the viewing rules
videoCallNoAsk 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
kindNo"task" or "event"; see Task or Event
connectedAccountIdNoWhich 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
calendarIdNoA 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.
  • :id may be viewing:<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, whose leadIds lists 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

StatuscodeWhen
400CALENDAR_TZ_REQUIRED / CALENDAR_TZ_INVALIDNo zone, or not an IANA zone
400CALENDAR_WINDOW_INVALID / CALENDAR_WINDOW_TOO_WIDEBad dates, or more than 42 days
400CALENDAR_LEAD_NO_EMAILInviting a lead with no email
400CALENDAR_TASK_NEEDS_LEADkind: "task" with no lead
400CALENDAR_TARGET_INVALIDconnectedAccountId or calendarId is not one of the owner's writable calendars
400CALENDAR_TEAMMATE_UNKNOWNA teammateIds entry who is not an active member of your organization
400CALENDAR_OWNER_NOT_ON_EVENTownerUserId is not one of teammateIds
403CALENDAR_OWNER_NOT_ALLOWEDA member naming someone else in ownerUserId
400CALENDAR_EVENT_READ_ONLYYour own Google or Outlook appointment, or a repeating event
400CALENDAR_EVENT_OPEN_HOUSEChange or cancel an open house from the open house
400CALENDAR_EVENT_VIEWINGMark a viewing done on the viewing itself
403CALENDAR_EVENT_NOT_YOURSSomeone else's event
404CALENDAR_EVENT_NOT_FOUNDNo such event, or not one you may see
409CALENDAR_NOT_CONNECTEDAttendees, or a retry, without a connected calendar
409CALENDAR_EVENT_CANCELLEDThe event is cancelled
409CALENDAR_VIEWING_NOT_MIRROREDThe viewing was saved but its event was not; try again