In short
- Get available slots: GET /calendars/{calendarId}/free-slots with a date range.
- Book an appointment: POST /calendars/events/appointments with slotStartTime, contactId, and calendarId.
- Get all calendar IDs for a location: GET /calendars/?locationId=X.
- The booked appointment triggers any GHL workflow with an "Appointment" trigger automatically.
Endpoints
| Method | Path | Scopes | Rate limit |
|---|---|---|---|
| GET | /calendars/ | calendars.readonly | See GHL API docs |
| GET | /calendars/{calendarId} | calendars.readonly | See GHL API docs |
| GET | /calendars/{calendarId}/free-slots | calendars.readonly | See GHL API docs |
| POST | /calendars/events/appointments | calendars.write | See GHL API docs |
| PUT | /calendars/events/appointments/{eventId} | calendars.write | See GHL API docs |
| DELETE | /calendars/events/{eventId} | calendars.write | See GHL API docs |
Authentication
Use your sub-account Bearer token (private integration token or OAuth access token). Include Version: 2021-07-28 in every request header.
Your token must have the calendars.readonly scope to fetch slots and calendars.write to create appointments.
The calendarId is the unique ID of the specific calendar you want to book into. Fetch it with GET /calendars/?locationId=YOUR_LOCATION_ID. Do not confuse it with the locationId (sub-account ID).
Slot times are returned and accepted in ISO 8601 format in UTC. Convert to the user's local timezone for display, but always send UTC back to the API.
Recipes
List available time slots for a calendar
Fetch open slots by passing the calendarId plus a startDate and endDate. The response returns a map of dates to arrays of available time windows. You can filter by timezone for display purposes.
Slots are only returned for dates that have at least one open window within the calendar's working hours and after any existing bookings are subtracted.
cURL
curl -G https://services.leadconnectorhq.com/calendars/CALENDAR_ID/free-slots \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
--data-urlencode "startDate=2026-06-01" \
--data-urlencode "endDate=2026-06-07" \
--data-urlencode "timezone=America/New_York"Node.js (fetch)
const params = new URLSearchParams({
startDate: '2026-06-01',
endDate: '2026-06-07',
timezone: 'America/New_York',
});
const res = await fetch(
`https://services.leadconnectorhq.com/calendars/CALENDAR_ID/free-slots?${params}`,
{
headers: {
Authorization: 'Bearer YOUR_TOKEN',
Version: '2021-07-28',
},
}
);
const data = await res.json();
// data._dates_ is an object keyed by date string
for (const [date, slots] of Object.entries(data._dates_ ?? {})) {
console.log(date, slots);
}Python (requests)
import requests
res = requests.get(
"https://services.leadconnectorhq.com/calendars/CALENDAR_ID/free-slots",
headers={"Authorization": "Bearer YOUR_TOKEN", "Version": "2021-07-28"},
params={
"startDate": "2026-06-01",
"endDate": "2026-06-07",
"timezone": "America/New_York",
},
)
res.raise_for_status()
slots_by_date = res.json().get("_dates_", {})
for date, slots in slots_by_date.items():
print(date, slots)Response (200 OK)
{
"_dates_": {
"2026-06-02": [
{ "startTime": "2026-06-02T14:00:00+00:00", "endTime": "2026-06-02T14:30:00+00:00" },
{ "startTime": "2026-06-02T15:00:00+00:00", "endTime": "2026-06-02T15:30:00+00:00" }
],
"2026-06-03": [
{ "startTime": "2026-06-03T13:00:00+00:00", "endTime": "2026-06-03T13:30:00+00:00" }
]
}
}Book an appointment
Once the user picks a slot, POST to /calendars/events/appointments. You need the calendarId, the contactId (the person being booked), and the slotStartTime from the free-slots response.
The appointmentStatus field defaults to "confirmed". Set it to "new" if you want the appointment to show as pending until the contact confirms via the GHL reminder workflow.
cURL
curl -X POST https://services.leadconnectorhq.com/calendars/events/appointments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-d '{
"calendarId": "CALENDAR_ID",
"locationId": "YOUR_LOCATION_ID",
"contactId": "CONTACT_ID",
"startTime": "2026-06-02T14:00:00+00:00",
"endTime": "2026-06-02T14:30:00+00:00",
"title": "Discovery Call",
"appointmentStatus": "confirmed",
"assignedUserId": "USER_ID"
}'Node.js (fetch)
const res = await fetch(
'https://services.leadconnectorhq.com/calendars/events/appointments',
{
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json',
Version: '2021-07-28',
},
body: JSON.stringify({
calendarId: 'CALENDAR_ID',
locationId: 'YOUR_LOCATION_ID',
contactId: 'CONTACT_ID',
startTime: '2026-06-02T14:00:00+00:00',
endTime: '2026-06-02T14:30:00+00:00',
title: 'Discovery Call',
appointmentStatus: 'confirmed',
}),
}
);
const { appointment } = await res.json();
console.log('Booked:', appointment.id);Python (requests)
import requests
res = requests.post(
"https://services.leadconnectorhq.com/calendars/events/appointments",
headers={
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json",
"Version": "2021-07-28",
},
json={
"calendarId": "CALENDAR_ID",
"locationId": "YOUR_LOCATION_ID",
"contactId": "CONTACT_ID",
"startTime": "2026-06-02T14:00:00+00:00",
"endTime": "2026-06-02T14:30:00+00:00",
"title": "Discovery Call",
"appointmentStatus": "confirmed",
},
)
res.raise_for_status()
print(res.json()["appointment"]["id"])Reschedule or cancel an appointment
To reschedule, PUT /calendars/events/appointments/{eventId} with the new startTime and endTime. To cancel, DELETE /calendars/events/{eventId}. Both actions trigger any GHL workflows listening for appointment status changes.
cURL (reschedule)
curl -X PUT https://services.leadconnectorhq.com/calendars/events/appointments/EVENT_ID \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-d '{
"startTime": "2026-06-03T13:00:00+00:00",
"endTime": "2026-06-03T13:30:00+00:00",
"appointmentStatus": "confirmed"
}'cURL (cancel)
curl -X DELETE https://services.leadconnectorhq.com/calendars/events/EVENT_ID \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28"Common errors
- 400 Slot not available: the slot you requested is no longer free. Another booking took it between your free-slots call and your POST. Always handle this in your UI and offer alternatives.
- 404 Calendar not found: the calendarId does not belong to the location in your token. Confirm the IDs with GET /calendars/?locationId=X.
- 422 Missing required field: startTime, endTime, contactId, calendarId, and locationId are all required on POST. Double-check the ISO 8601 format for times.
- 409 Conflict: the contact already has an appointment in this calendar at this time. Check before booking or catch the error and handle it gracefully.
MCP tool definition
MCP tool definition (JSON)
{
"name": "ghl_book_appointment",
"description": "Book an appointment in a GoHighLevel calendar for an existing contact.",
"inputSchema": {
"type": "object",
"properties": {
"calendarId": { "type": "string" },
"contactId": { "type": "string" },
"locationId": { "type": "string" },
"startTime": { "type": "string", "description": "ISO 8601 UTC datetime" },
"endTime": { "type": "string", "description": "ISO 8601 UTC datetime" },
"title": { "type": "string" }
},
"required": ["calendarId", "contactId", "locationId", "startTime", "endTime"]
}
}