> ## Documentation Index
> Fetch the complete documentation index at: https://docs.teamfollowup.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, campaigns, projects (GoHighLevel sub-accounts), contacts, calls, outcomes, skills, cadences, phone numbers.
> Scope requests by locationId rather than projectName.
> Read the guide linked from each API reference group before recommending an endpoint.

# Calendars and booking

> The diary a sub-account books into when it has no CRM: opening hours, slots and seats, appointments, and calendars that live in another product.

Booking is the one thing a voice agent does that needs somewhere to put the
appointment. A sub-account with GoHighLevel books into a GHL calendar. A
sub-account without one books into a **Team Follow Up calendar**, described
here.

The two are the same shape. Availability comes back as days of
start times either way, and a booking answers with the time it actually took,
so an integration written against one reads the other unchanged.

## What a calendar is

A calendar is a bookable resource with hours and rules. A sub-account can have
several, and a campaign names the one its agent books into.

| Setting               | What it does                                                                                  | Default |
| --------------------- | --------------------------------------------------------------------------------------------- | ------- |
| `timezone`            | The calendar's own zone. Opening hours are read in it, and a booking anchors to it. Required. | none    |
| `weeklyHours`         | `{ mon: [{ start: "09:00", end: "17:00" }], ... }`. A day left out is closed.                 | none    |
| `dateOverrides`       | Named days that differ: `{ date, closed: true }`, or different windows.                       | none    |
| `slotDurationMinutes` | How long an appointment lasts.                                                                | `30`    |
| `slotIntervalMinutes` | How often a slot starts. Shorter than the duration gives overlapping starts.                  | `30`    |
| `slotBufferMinutes`   | Quiet time held either side of a booking.                                                     | `0`     |
| `appointmentsPerSlot` | Seats per slot. More than one is a group booking.                                             | `1`     |
| `minNoticeMinutes`    | How soon the first offered slot may be.                                                       | `180`   |
| `maxDaysOut`          | How far ahead it will offer.                                                                  | `31`    |

The defaults match what a GoHighLevel calendar does, so an agent moved between
the two behaves the same.

<Note>
  Hours are validated as a whole. A window that ends at or before it starts is
  refused rather than wrapped past midnight, because a slot that crossed
  midnight would land under a different date than anyone expects. On an update
  the **merged** calendar is checked, not your patch, so dropping every window,
  or making the appointment longer than the shortest window it must fit in, is
  refused with the reason.
</Note>

## Creating one

```bash theme={"dark"}
curl -X POST "https://api.teamfollowup.ai/api/native-calendar/calendars?locationId={locationId}" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Consultations",
    "timezone": "America/Chicago",
    "weeklyHours": {
      "mon": [{"start":"09:00","end":"12:00"},{"start":"13:00","end":"17:00"}],
      "tue": [{"start":"09:00","end":"17:00"}]
    },
    "slotDurationMinutes": 30,
    "minNoticeMinutes": 180
  }'
```

Try a definition before you save it with
[Preview slots](/api-reference/native-calendar/preview-slots-for-a-calendar-definition):
it validates the definition and returns the slots it would offer. Pass an
existing `calendarId` as well and what is already booked is counted, so the
preview shows what would really be left.

## Slots and seats

[Free slots](/api-reference/native-calendar/get-free-slots) answers day by day:

```json theme={"dark"}
{
  "success": true,
  "data": {
    "timezone": "America/Chicago",
    "slots": {
      "2026-09-24": { "slots": ["2026-09-24T14:00:00.000Z", "2026-09-24T14:30:00.000Z"] },
      "2026-09-25": { "slots": ["2026-09-25T14:00:00.000Z"] }
    }
  }
}
```

It is computed per request against live bookings and is never cached, because a
cached slot is one somebody else may already have taken.

A slot holds as many seats as `appointmentsPerSlot`. `confirmed` and `showed`
occupy a seat; `cancelled` and `noshow` free it. A past appointment that
happened keeps its seat, so a second booking can never land on a slot that was
genuinely used.

## Booking

```bash theme={"dark"}
curl -X POST "https://api.teamfollowup.ai/api/native-calendar/appointments?locationId={locationId}" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"calendarId":"cal_...","contactId":"nct_...","startTime":"2026-09-24T15:00:00.000Z"}'
```

Three things are worth knowing before you write the call:

* **`startTime` without an offset is read in the calendar's zone.** Epoch
  milliseconds and a full ISO instant are both accepted and are unambiguous.
  A bare `2026-09-24T15:00:00` is read as 15:00 where the calendar lives, which
  is usually what you meant.
* **The seat is claimed as the booking is written, not checked beforehand**, so
  two callers racing for the last seat cannot both win. The loser gets `409`
  with `errorType: "slot_unavailable"`. A time outside the hours, inside the
  notice window or past the lookahead comes back as `invalid_time`.
* **Read `bookedStartTime` from the response** rather than assuming the time
  you asked for is the time it took.

Booking ends any chase that contact was in, exactly as when an agent books them
on a call, and starts the confirmation campaign on that calendar.

[Rescheduling](/api-reference/native-calendar/reschedule-an-appointment) claims
the new time the same way and only gives up the old seat once the new one is
held. The reminders that belonged to the old time are re-anchored to the new
one rather than left pointing at a time that has passed.

## Time that is not a booking

An [event](/api-reference/native-calendar/add-an-event) is an entry with no
contact: a meeting, a reminder, an hour held back. It carries no lead, starts
no campaign and tells nobody. What it does do is occupy the seats it covers, so
the agent stops offering that time.

## Retiring a calendar

A delete refuses while people still have appointments on it, answering `409`
with `upcomingCount`, so a calendar cannot quietly strand everyone who booked.
`force=true` goes ahead and cancels them, and cancelling a booking tells the
attendee.

## Calendars that live somewhere else

A sub-account can connect a calendar product it already uses, such as Cal.com.
[List connections](/api-reference/native-calendar/list-calendar-connections)
says which calendar systems can be connected, how each one is connected, and
whether this sub-account has done it.

<Steps>
  <Step title="Paste the credential">
    [Connect a calendar system](/api-reference/native-calendar/connect-a-calendar-system)
    stores the key. It is not checked against that product at the moment you
    save it: a key that is right today and revoked tomorrow reads the same, so
    a connection is proven where it is used.
  </Step>

  <Step title="Cancellations are registered at connect time">
    A cancellation made in that product reaches us only because the connection
    registered for it. The connection row says whether yours is registered.
    Reconnect the system if it is not.
  </Step>

  <Step title="Read, never write">
    Their calendars and bookings are read through their own API and shown
    beside yours. Nothing here writes into that product, because the calendar
    is managed there.
  </Step>
</Steps>

<Note>
  A reconnect replaces the whole connection and removes what was registered
  under the old credential first, while that credential can still authenticate
  the removal. Otherwise it stays live in the customer's account and every
  cancellation arrives twice.
</Note>

## Where to go next

<CardGroup cols={2}>
  <Card title="Why the agent found nothing" icon="search-x" href="/calendars/availability">
    The two settings behind almost every empty diary.
  </Card>

  <Card title="Native Calendar API" icon="code" href="/api-reference/native-calendar">
    Every operation, with schemas and examples.
  </Card>
</CardGroup>
