Appointments
The Appointments API lets you book appointments for your contacts on your event types, then fetch, list, update, cancel, or delete them. It also answers the question that comes first in most booking flows — which times are actually free — and covers the calendar side: listing the Google Calendars you have connected and importing events that already live in them. When a Google Calendar connection is active, the matching calendar event is created and kept in sync automatically in the background. Restaurants using Zenchef, Formitable, OpenTable or TheFork for their own reservations system can also be verified and connected here, so the AI Agent books real tables instead of internal appointments — and appointment businesses running their schedule in Trafft can connect that the same way.
All paths on this page are relative to the base URL https://api.youraiconnector.com/v1. Every request needs your API key — see Authentication for the full list of ways to send it. The examples below use the X-API-Key header, with one cURL example showing the ?apiKey= query form too.
Events vs. appointments: An event type is a bookable slot definition (the kind of meeting, its length, its rooms). An appointment is one booked instance of an event type for a specific contact. You book an appointment by referencing the contact and the event type.
The appointment object
Every endpoint that returns an appointment uses the same shape:
| Field | Description |
|---|---|
id |
Unique ID of the appointment. |
contact_id |
ID of the contact the appointment is booked with. |
event_id |
ID of the event type the appointment was booked on. null for external bookings — appointments recorded without an event type. |
status |
Confirmed or Canceled. |
start_time |
Start of the appointment, ISO 8601 in UTC. |
end_time |
End of the appointment, ISO 8601 in UTC. |
created_at |
When the appointment was created. |
last_modified_at |
When the appointment was last changed. |
room_name |
Room or resource the appointment is booked in, when the event type uses rooms. |
description |
Free-form description of the appointment. |
summary |
Short summary or title. For an external booking this is the title it was recorded with. |
external_reference |
Your own ID for this booking (from your CRM or booking system), as you sent it, or null. |
cancelation_reason |
Reason supplied when the appointment was canceled, if any. |
google_calendar_event_id |
ID of the linked Google Calendar event. Set once calendar sync completes; null when no calendar is connected or while the sync is still in progress. |
calendar_synced |
true once the appointment is linked to a calendar event. |
imported |
true when the appointment was imported from an external calendar rather than booked directly. |
is_recurring |
true when the appointment is part of a recurring series. |
recurrence_frequency |
How often the appointment repeats, when recurring. |
recurring_event_id |
ID of the recurring series this appointment belongs to. |
recurring_interval |
Interval between repetitions, when recurring. |
recurring_sequence |
Position of this appointment within its recurring series. |
end_after_x_occurrences |
Number of occurrences after which the recurring series ends. |
booking_provider |
Source system the booking came from, when booked through a connected reservation provider. |
About calendar sync: Right after you book or change an appointment,
google_calendar_event_idmay still benullandcalendar_syncedmay befalsebecause the sync runs in the background a moment later. Fetch the appointment again shortly afterward to see the populated calendar fields.
Find available slots
GET /appointments/available-slots
Returns the times that are genuinely free on one event type between two moments. This is normally the first call in a booking flow: show these slots, let the person pick one, then post the chosen time to Book an appointment.
The answer already takes into account the event type’s own opening hours and slot length, its rooms, appointments you have already booked on it, and everything blocked on the connected Google Calendars — so a slot that comes back here is one you can book.
| Query parameter | Required | Description |
|---|---|---|
event_id |
Yes | The event type to check. Must belong to your account. Find it in the app by opening the event type on the Appointments page (Event Types tab): the edit window shows its Event type ID with a copy button. |
start_time |
Yes | Start of the window you want slots for, ISO 8601 date-time. |
end_time |
Yes | End of the window, ISO 8601 date-time. The whole end day is included. |
Results come back grouped by day — and, when the event type uses rooms, one group per room per day:
| Field | Description |
|---|---|
date |
The day the group covers, written DD/MM/YYYY. |
day |
Weekday name in lower case, for example monday. |
room_name |
The room or resource this group belongs to, when the event type uses rooms. |
available_slots |
The bookable blocks on that day, earliest first. |
Each entry in available_slots has:
| Field | Description |
|---|---|
start_time |
Block start as HH:mm. |
end_time |
Block end as HH:mm. |
available |
true — only free time is returned. |
spots_left |
How many bookings still fit in this block. Only present on event types that take more than one booking per slot. |
Times are local to the event type, not UTC.
date,start_time, andend_timeare wall-clock values in the event type’s own timezone (its override, or your account timezone when it has none). Book an appointment expects an ISO 8601 UTC instant, so convert the slot you picked before posting it.
cURL
curl "https://api.youraiconnector.com/v1/appointments/available-slots?event_id=event_xyz789&start_time=2026-06-15T00:00:00.000Z&end_time=2026-06-19T00:00:00.000Z" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const params = new URLSearchParams({
event_id: "event_xyz789",
start_time: "2026-06-15T00:00:00.000Z",
end_time: "2026-06-19T00:00:00.000Z",
});
const res = await fetch(
`https://api.youraiconnector.com/v1/appointments/available-slots?${params}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.data);
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/appointments/available-slots",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"event_id": "event_xyz789",
"start_time": "2026-06-15T00:00:00.000Z",
"end_time": "2026-06-19T00:00:00.000Z",
},
)
print(res.json()["data"])
Response (200 OK):
{
"success": true,
"data": [
{
"date": "15/06/2026",
"day": "monday",
"room_name": "Room A",
"available_slots": [
{ "start_time": "10:00", "end_time": "10:30", "available": true },
{ "start_time": "10:30", "end_time": "11:00", "available": true }
]
},
{
"date": "16/06/2026",
"day": "tuesday",
"room_name": "Room A",
"available_slots": [
{ "start_time": "09:00", "end_time": "09:30", "available": true, "spots_left": 2 }
]
}
]
}
A day with nothing free simply does not appear. Missing event_id, start_time, or end_time returns 400; an event type that is not on your account returns 404.
Book an appointment
POST /appointments
Creates an appointment for one of your contacts. There are two ways to use it:
- On one of your event types — send
event_id, and the booking works exactly as it always has: availability and capacity are respected, the end time is calculated from the event type’s slot duration, and the appointment syncs to your Google Calendar when one is connected. - As an external booking — leave
event_idout and send atitleinstead. This records a booking that was made somewhere else (your own CRM, a class schedule, another booking tool) so that it shows up on the Appointments page alongside the rest.
A booking on an event type with rooms must be for a time that Find available slots would offer: inside the room’s hours, outside the notice period, within how far ahead the event type can be booked, on the slot grid, not in the past, and not blocked on a connected calendar. Any other time is refused with a 400 and nothing is created. So is a room_name that matches none of the event type’s rooms (the message lists the valid names). Buffer times set on the event type (Buffer before and Buffer after in the event type editor, buffer_before and buffer_after in minutes) are kept free around every booking.
A booking on an event type is also conflict-checked: if the slot is already full, the request fails with a 409 and nothing is created. An external booking has no event type behind it, so nothing is checked against your availability or capacity and nothing is synced to a calendar.
Retrying is safe. If the same contact already has a confirmed booking on the same event type at the same start (in the same room, when you name one), for example because your first request timed out after it went through, you get that booking back with a 200 and "replayed": true instead of a second booking or a 409.
An external_reference works as an idempotency key. It is unique on your account: if an appointment already carries the reference you send, that appointment comes back with a 200 and "replayed": true and nothing new is created, even when the retry names a different time or contact and whatever that appointment’s status is. Use it for paid bookings (send your payment or order ID), and send a new reference for a genuinely new booking.
| Field | Required | Description |
|---|---|---|
contact_id |
Yes | ID of the contact to book for. Must belong to your account. |
start_time |
Yes | Desired start as an ISO 8601 date-time. A time sent without a timezone offset is read in your account’s timezone. |
event_id |
No | ID of the event type to book on. Must belong to your account. Leave it out to record an external booking. |
title |
Only without event_id |
The name the appointment is shown under on the Appointments page — the class name, for example. Up to 200 characters. It comes back as summary. |
end_time |
No | End of an external booking as an ISO 8601 date-time, later than start_time. Not needed with an event_id: the end time then comes from the event type’s slot duration. |
duration_minutes |
No | How long an external booking lasts, a whole number of minutes from 1 to 1440. Use this or end_time, not both. With neither, an external booking lasts 60 minutes. |
description |
No | Free-form notes to keep with the appointment. |
room_name |
No | Room or resource name, when the event type uses rooms. Must match one of its rooms. Leave it out to book the first free room. |
external_reference |
No | Your own ID for this booking, so you can match it back to the record in your CRM or booking system. Up to 255 characters. It comes back on every read and in the Appointment Booked, Cancelled and Rescheduled webhooks. Unique on your account: a reference that is already in use returns that appointment instead of booking again (see above). |
Send event_id and title together, or neither of the two, and the request returns a 400 — as does sending both end_time and duration_minutes.
cURL (using the ?apiKey= query form)
curl -X POST "https://api.youraiconnector.com/v1/appointments?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"start_time": "2026-06-15T10:00:00.000Z",
"room_name": "Room A"
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/appointments", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
contact_id: "contact_abc123",
event_id: "event_xyz789",
start_time: "2026-06-15T10:00:00.000Z",
room_name: "Room A",
}),
});
const data = await res.json();
console.log(data.appointment_id);
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/appointments",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"start_time": "2026-06-15T10:00:00.000Z",
"room_name": "Room A",
},
)
print(res.json()["appointment_id"])
Response (201 Created):
{
"success": true,
"appointment_id": "aBcD1234eFgH5678",
"appointment": {
"id": "aBcD1234eFgH5678",
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"status": "Confirmed",
"start_time": "2026-06-15T10:00:00.000Z",
"end_time": "2026-06-15T10:30:00.000Z",
"created_at": "2026-06-10T09:00:00.000Z",
"last_modified_at": "2026-06-10T09:00:00.000Z",
"room_name": "Room A",
"google_calendar_event_id": null,
"calendar_synced": false
}
}
Recording a booking made somewhere else
Same endpoint, without an event type: a title and a start_time are enough, and duration_minutes sets how long it runs.
cURL
curl -X POST "https://api.youraiconnector.com/v1/appointments" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_abc123",
"title": "Beginner guitar class",
"start_time": "2026-06-15T10:00:00.000Z",
"duration_minutes": 90,
"external_reference": "class-slot-4812"
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/appointments", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
contact_id: "contact_abc123",
title: "Beginner guitar class",
start_time: "2026-06-15T10:00:00.000Z",
duration_minutes: 90,
}),
});
const data = await res.json();
console.log(data.appointment_id);
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/appointments",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"contact_id": "contact_abc123",
"title": "Beginner guitar class",
"start_time": "2026-06-15T10:00:00.000Z",
"duration_minutes": 90,
},
)
print(res.json()["appointment_id"])
Response (201 Created):
{
"success": true,
"appointment_id": "eFgH5678aBcD1234",
"appointment": {
"id": "eFgH5678aBcD1234",
"contact_id": "contact_abc123",
"event_id": null,
"status": "Confirmed",
"start_time": "2026-06-15T10:00:00.000Z",
"end_time": "2026-06-15T11:30:00.000Z",
"created_at": "2026-06-10T09:00:00.000Z",
"last_modified_at": "2026-06-10T09:00:00.000Z",
"summary": "Beginner guitar class",
"google_calendar_event_id": null,
"calendar_synced": false
}
}
External bookings are ordinary appointments from there on: they appear on the Appointments page and in List appointments, they can be rescheduled, cancelled or deleted in the app or through this API, the booking notification email goes out as usual, and the Appointment booked automation trigger and webhook fire for them like for any other booking made through the API. The only differences are the ones above — no availability or capacity check, no calendar sync, and event_id stays null.
Get an appointment
GET /appointments/{appointmentId}
Returns a single appointment by its ID, including its calendar sync state.
cURL
curl "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointment);
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["appointment"])
Response (200 OK):
{
"success": true,
"appointment": {
"id": "aBcD1234eFgH5678",
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"status": "Confirmed",
"start_time": "2026-06-15T10:00:00.000Z",
"end_time": "2026-06-15T10:30:00.000Z",
"room_name": "Room A",
"google_calendar_event_id": "abc123googleevent",
"calendar_synced": true
}
}
List appointments
GET /appointments
Lists appointments for your account, newest first, with cursor-based pagination.
| Query parameter | Required | Description |
|---|---|---|
contact_id |
No | Only return appointments for this contact. Contact-filtered listings include confirmed appointments only. |
date |
No | Only return appointments on this calendar day (YYYY-MM-DD). Requires contact_id. |
status |
No | Filter by Confirmed or Canceled. Only available without contact_id. |
limit |
No | Page size, an integer between 1 and 100. Default 50. |
cursor |
No | The next_cursor value from a previous response. |
A few rules to keep in mind:
- Without filters, you get every appointment on the account, page by page.
- By contact — set
contact_idto see one contact’s confirmed appointments. You can narrow this to a single day by also passingdate. - By status — set
status(withoutcontact_id) to list onlyConfirmedor onlyCanceledappointments across the account. - The
datefilter withoutcontact_id, orstatus=Canceledtogether withcontact_id, returns a400.
cURL
curl "https://api.youraiconnector.com/v1/appointments?contact_id=contact_abc123&date=2026-06-15" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const params = new URLSearchParams({
contact_id: "contact_abc123",
date: "2026-06-15",
});
const res = await fetch(
`https://api.youraiconnector.com/v1/appointments?${params}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointments, data.next_cursor);
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/appointments",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"contact_id": "contact_abc123", "date": "2026-06-15"},
)
data = res.json()
print(data["appointments"], data["next_cursor"])
Response (200 OK):
{
"success": true,
"appointments": [
{
"id": "aBcD1234eFgH5678",
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"status": "Confirmed",
"start_time": "2026-06-15T10:00:00.000Z",
"end_time": "2026-06-15T10:30:00.000Z",
"calendar_synced": true
}
],
"next_cursor": null
}
To page through results, pass the next_cursor from one response as the cursor of the next request. Keep going until next_cursor is null. See Errors & Pagination for the shared pagination pattern.
Update an appointment
PUT /appointments/{appointmentId}
Reschedule an appointment or change its details. Send only the fields you want to change — at least one is required. The combined start and end must stay in chronological order (end_time must be after start_time). Changes are synced to the linked calendar event automatically.
Moving a confirmed booking on an event type (a new start_time or room_name) is conflict-checked like a new booking: a slot that is already full answers 409 and nothing changes, and a room_name that matches none of the event type’s rooms answers 400. Send only start_time and the booking keeps its length. Your own opening hours and notice period are not applied here, so you can still move a booking to any free time.
| Field | Description |
|---|---|
start_time |
New start, ISO 8601 date-time. A time sent without a timezone offset is read in the event type’s timezone (or your account’s), the same as when booking. |
end_time |
New end, ISO 8601 date-time, read the same way. Must be after the start time. |
room_name |
New room or resource name. |
description |
New description, or null to clear it. |
summary |
New summary, or null to clear it. |
external_reference |
New reference (up to 255 characters), or null to clear it. A reference another appointment already has is refused with a 409 and "code": "external_reference_taken". |
cURL
curl -X PUT "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"start_time": "2026-06-16T10:00:00.000Z",
"end_time": "2026-06-16T10:30:00.000Z"
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
start_time: "2026-06-16T10:00:00.000Z",
end_time: "2026-06-16T10:30:00.000Z",
}),
}
);
const data = await res.json();
console.log(data.appointment);
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"start_time": "2026-06-16T10:00:00.000Z",
"end_time": "2026-06-16T10:30:00.000Z",
},
)
print(res.json()["appointment"])
Response (200 OK):
{
"success": true,
"appointment_id": "aBcD1234eFgH5678",
"appointment": {
"id": "aBcD1234eFgH5678",
"contact_id": "contact_abc123",
"event_id": "event_xyz789",
"status": "Confirmed",
"start_time": "2026-06-16T10:00:00.000Z",
"end_time": "2026-06-16T10:30:00.000Z",
"calendar_synced": true
}
}
Cancel an appointment
POST /appointments/{appointmentId}/cancel
Cancels a confirmed appointment, optionally recording a reason. The appointment stays in your account with status Canceled, and the linked calendar event is removed automatically in the background. Cancelling an already-canceled appointment returns a 400.
| Field | Required | Description |
|---|---|---|
cancellation_reason |
No | Reason for the cancellation, stored on the appointment. |
cURL
curl -X POST "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cancellation_reason": "Client asked to reschedule next month"
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel",
{
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
cancellation_reason: "Client asked to reschedule next month",
}),
}
);
const data = await res.json();
console.log(data.success);
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={"cancellation_reason": "Client asked to reschedule next month"},
)
print(res.json()["success"])
Response (200 OK):
{
"success": true,
"appointment_id": "aBcD1234eFgH5678"
}
Resend the booking confirmation
POST /appointments/{appointmentId}/resend-confirmation
Sends the contact their booking confirmation again, for example when they say it never arrived or went to spam. Two parts, both sent unless you switch one off: the confirmation email (the same email the contact got when they booked) and, when the appointment is on your Google Calendar, the Google Calendar invitation. Both go to the email address on the contact.
| Field | Required | Description |
|---|---|---|
email |
No | false skips the confirmation email. Defaults to true. |
calendar_invite |
No | false skips the Google Calendar invitation. Defaults to true. |
Each part comes back with its own outcome: sent, not_requested, no_contact_email, turned_off_for_event_type (the event type’s Send appointment email to contact switch is off), not_on_google_calendar, imported (appointments imported from your calendar get no confirmation email) or failed.
200when at least one part was sent.recipientis the address it went to.409for a canceled appointment, one that is already over, or a booking made through Zenchef, Formitable, OpenTable, TheFork or Trafft (that system sends its own confirmation).codeisappointment_canceled,appointment_in_pastormanaged_by_provider.422when nothing could be sent, with the reason inerror.
cURL
curl -X POST "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/resend-confirmation" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/resend-confirmation",
{
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ calendar_invite: false }),
}
);
const data = await res.json();
console.log(data.email, data.recipient);
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/resend-confirmation",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={},
)
print(res.json())
Response (200 OK):
{
"success": true,
"appointment_id": "aBcD1234eFgH5678",
"email": "sent",
"calendar_invite": "sent",
"recipient": "jane@example.com"
}
Delete an appointment
DELETE /appointments/{appointmentId}
Permanently deletes an appointment and its references. If you only want to call the booking off while keeping the record, use cancel instead.
cURL
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
{ method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.success);
Python
import requests
res = requests.delete(
"https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
Response (200 OK):
{
"success": true
}
List your connected Google Calendars
GET /appointments/google-calendars
Returns the Google Calendars available on this account, straight from Google — useful for showing the account holder a picker of which calendar to import from below, or just to confirm the connection is live.
This only works once the account has connected Google Calendar (Settings → Integrations) with at least read access. If it hasn’t, or the granted access no longer includes the calendar-read scope, you get a 400 telling you to (re)connect it.
cURL
curl "https://api.youraiconnector.com/v1/appointments/google-calendars" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/appointments/google-calendars", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.data);
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/appointments/google-calendars",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["data"])
Response (200 OK):
{
"success": true,
"data": [
{
"id": "primary",
"summary": "jane@example.com",
"timeZone": "America/New_York",
"accessRole": "owner",
"primary": true
},
{
"id": "abcdefg1234567890@group.calendar.google.com",
"summary": "Bookings",
"timeZone": "America/New_York",
"accessRole": "writer"
}
]
}
Each entry is Google’s own CalendarListEntry shape, so field names follow Google’s camelCase, not this API’s usual snake_case — that’s Google’s data passed through as-is, not ours. A missing or revoked connection returns 400 with an error explaining Google Calendar needs to be (re)connected.
Import events from a Google Calendar
POST /appointments/import-calendar-events
Pulls the events already sitting in a campaign’s or AI Agent’s connected Google Calendar(s) and turns them into appointments — useful the first time you connect a calendar that already has bookings on it. This can take a while (each event goes through extraction to figure out who it’s for), so it never runs inline: the request enqueues a background job and hands you back a job_id to poll.
| Field | Required | Description |
|---|---|---|
campaign_id |
One of these two | The campaign whose connected calendar(s) to import from. |
agent_id |
One of these two | The AI Agent whose connected calendar(s) to import from. |
identifier |
Yes | "EMAIL" or "PHONE_NUMBER" — which piece of contact info to extract from each calendar event to match or create the contact it belongs to. |
Send exactly one of campaign_id / agent_id, never both and never neither — either combination returns a 400. Whichever one you send must belong to your account, or you get a 404.
cURL
curl -X POST "https://api.youraiconnector.com/v1/appointments/import-calendar-events?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent_abc123",
"identifier": "EMAIL"
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/appointments/import-calendar-events", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
agent_id: "agent_abc123",
identifier: "EMAIL",
}),
});
const data = await res.json();
console.log(data.job_id);
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/appointments/import-calendar-events",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={"agent_id": "agent_abc123", "identifier": "EMAIL"},
)
print(res.json()["job_id"])
Response (202 Accepted):
{
"success": true,
"job_id": "jK9mQ2xR7pL4wN1t",
"status": "queued",
"campaign_id": null,
"agent_id": "agent_abc123"
}
campaign_id and agent_id echo back whichever one you sent; the other is always null.
Poll the import job
GET /appointments/import-calendar-events/{jobId}
curl "https://api.youraiconnector.com/v1/appointments/import-calendar-events/jK9mQ2xR7pL4wN1t" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK):
{
"success": true,
"job_id": "jK9mQ2xR7pL4wN1t",
"status": "completed",
"message": "Imported 12 events as appointments.",
"error": null
}
status |
Meaning |
|---|---|
queued |
Not picked up yet. Keep polling. |
processing |
The import is running. Keep polling. |
completed |
Done — message has a short human-readable summary. |
failed |
Something went wrong — error has the reason. |
GET on a jobId that doesn’t exist (or belongs to a different account) returns 404.
External booking integrations (Zenchef / Formitable / OpenTable / TheFork / Trafft)
Zenchef and Formitable are restaurant reservation systems your AI Agent can book real tables through; Trafft is a scheduling platform for appointment businesses, connected once per account rather than per restaurant. The two restaurant platforms each have a public, unauthenticated booking widget (https://api.youraiconnector.com/v1/zenchef-widget/... and https://api.youraiconnector.com/v1/formitable-widget/...) that renders inside chat for the diner — those widget routes are plain HTML pages meant to be opened in a browser, not JSON API endpoints, so they aren’t documented here. What follows are the account-management endpoints: verifying a restaurant ID belongs to the account holder, then adding, updating, or removing it.
Zenchef
Connecting a Zenchef restaurant is a two-step verification, so the account holder proves they actually run the restaurant before it gets wired into the bot: first check the ID exists (without revealing the name), then have them type the restaurant’s name themselves and verify it matches.
Step 1 — Check a restaurant ID exists
POST /appointments/zenchef-restaurants/check
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The Zenchef restaurant ID to check. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/check?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "12345" }'
Response (200 OK):
{
"success": true,
"data": { "exists": true, "requiresNameVerification": true }
}
exists: false means no Zenchef restaurant has that ID — nothing else to do. Rate-limited to 10 checks per 5 minutes per account; going over returns 429.
Step 2 — Verify the restaurant’s name
POST /appointments/zenchef-restaurants/verify-name
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The Zenchef restaurant ID from step 1. |
user_input_name |
Yes | The name the account holder typed in — compared against the restaurant’s real name on Zenchef (case/whitespace-insensitive). |
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/verify-name?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "12345", "user_input_name": "The Blue Door Bistro" }'
Response (200 OK):
{
"success": true,
"data": {
"verified": true,
"restaurantDetails": {
"id": "12345",
"name": "The Blue Door Bistro",
"address": "1 Rue de Rivoli, Paris",
"status": "active"
}
}
}
verified: false means the name didn’t match — restaurantDetails is omitted, ask the account holder to try again. Rate-limited to 3 attempts per 5 minutes (tighter than the existence check, since this is the actual proof step). A restaurant_id that no longer resolves on Zenchef returns 404.
Step 3 — Save the restaurant
POST /appointments/zenchef-restaurants
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | 1–64 chars, letters/numbers/underscore/hyphen. |
restaurant_name |
Yes | The verified restaurant name from step 2. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "12345", "restaurant_name": "The Blue Door Bistro" }'
Response (201 Created):
{ "success": true, "data": { "restaurantId": "12345" } }
Update a saved Zenchef restaurant
PUT /appointments/zenchef-restaurants/{restaurantId}
| Field | Required | Description |
|---|---|---|
restaurant_name |
No | New display name. |
is_active |
No | Set false to stop the bot from booking against this restaurant without removing it. |
curl -X PUT "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/12345" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK): same shape as the save response above.
Remove a Zenchef restaurant
DELETE /appointments/zenchef-restaurants/{restaurantId}
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/12345" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK): { "success": true, "data": { "restaurantId": "12345" } }
A restaurantId not currently on the account returns 404 on update or delete.
Formitable
Formitable doesn’t need the two-step name proof Zenchef does — its restaurant IDs are already scoped per business, so one verification call is enough. It also has a details lookup used to cache the restaurant’s website URL during setup.
Verify a restaurant ID
POST /appointments/formitable-restaurants/verify
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The Formitable restaurant ID. |
language |
No | Language tag for the probe request. Defaults to "nl". |
curl -X POST "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/verify?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "the-blue-door", "language": "en" }'
Response (200 OK):
{
"success": true,
"data": {
"verified": true,
"restaurantDetails": {
"restaurantId": "the-blue-door",
"productCount": 4,
"sampleProductTitle": "Dinner for two",
"language": "en"
}
}
}
A restaurant_id Formitable doesn’t recognize returns 404. Rate-limited to 10 attempts per 5 minutes per account.
Get restaurant details
GET /appointments/formitable-restaurants/{restaurantId}/details?language=en
Fetches the restaurant’s public profile from Formitable, including its website — used to cache the website URL while setting the restaurant up. language is an optional query parameter, defaulting to "en".
curl "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/the-blue-door/details?language=en" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK):
{
"success": true,
"data": {
"uid": "the-blue-door",
"name": "The Blue Door Bistro",
"website": "https://thebluedoorbistro.com",
"email": "info@thebluedoorbistro.com",
"telephone": "+31201234567",
"streetAddress": "Prinsengracht 1",
"zipcode": "1015 AB",
"city": "Amsterdam",
"country": "Netherlands",
"countryCode": "NL",
"currency": "EUR"
}
}
Save the restaurant
POST /appointments/formitable-restaurants
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | 1–64 chars, letters/numbers/underscore/hyphen. |
restaurant_name |
Yes | Display name. |
language |
Yes | ISO language tag, e.g. "en" or "en-GB". |
website_url |
No | The restaurant’s website, from the details lookup above. Must be http(s)://. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/formitable-restaurants?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"restaurant_id": "the-blue-door",
"restaurant_name": "The Blue Door Bistro",
"language": "en",
"website_url": "https://thebluedoorbistro.com"
}'
Response (201 Created): { "success": true, "data": { "restaurantId": "the-blue-door" } }
Update a saved Formitable restaurant
PUT /appointments/formitable-restaurants/{restaurantId}
| Field | Required | Description |
|---|---|---|
restaurant_name |
No | New display name. |
language |
No | New ISO language tag. |
is_active |
No | Set false to stop the bot from booking against this restaurant without removing it. |
website_url |
No | New website URL. |
curl -X PUT "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/the-blue-door" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK): same shape as the save response above.
Remove a Formitable restaurant
DELETE /appointments/formitable-restaurants/{restaurantId}
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/the-blue-door" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK): { "success": true, "data": { "restaurantId": "the-blue-door" } }
A restaurantId not currently on the account returns 404 on update or delete.
OpenTable
OpenTable restaurants are reached through the platform’s OpenTable partner credentials, and OpenTable only lets those credentials see restaurants that connected the platform’s listing inside OpenTable’s Integrations Marketplace. So, like Formitable, one verification call is enough: a reachable Restaurant ID (the numeric “RID”) proves both that the restaurant exists and that it connected the integration. Until the OpenTable partner listing is enabled on the platform, the verify call answers 503.
Verify an OpenTable restaurant
POST /appointments/opentable-restaurants/verify
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The OpenTable Restaurant ID (RID), a number such as 1038007. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/opentable-restaurants/verify?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "1038007" }'
Response (200 OK):
{
"success": true,
"data": {
"verified": true,
"restaurantDetails": {
"restaurantId": "1038007",
"diningAreaCount": 2,
"diningAreaNames": ["Main Room", "Garden"],
"tableTypes": ["default", "outdoor", "bar"]
}
}
}
verified: false means no OpenTable restaurant has that ID. A 403 means the restaurant exists but has not connected the platform’s integration inside OpenTable yet. Rate-limited to 10 attempts per 5 minutes per account.
Add an OpenTable restaurant
POST /appointments/opentable-restaurants
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The verified Restaurant ID. |
restaurant_name |
Yes | Display name (a label; also what the AI calls the restaurant). |
website_url |
No | An http(s) URL shown to guests when the AI hands them off to the restaurant. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/opentable-restaurants?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "1038007", "restaurant_name": "The Blue Door", "website_url": "https://thebluedoor.example" }'
Response (201 Created): { "success": true, "data": { "restaurantId": "1038007" } }
Update a saved OpenTable restaurant
PUT /appointments/opentable-restaurants/{restaurantId}
Send any of restaurant_name, is_active (pause with false) or website_url (empty string clears it); omitted fields are left unchanged.
curl -X PUT "https://api.youraiconnector.com/v1/appointments/opentable-restaurants/1038007" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK): { "success": true, "data": { "restaurantId": "1038007" } }
Remove an OpenTable restaurant
DELETE /appointments/opentable-restaurants/{restaurantId}
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/opentable-restaurants/1038007" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK): { "success": true, "data": { "restaurantId": "1038007" } }
A restaurantId not currently on the account returns 404 on update.
TheFork
TheFork restaurants are reached through the platform’s TheFork partner credentials, and TheFork only lets those credentials see restaurants that have the platform’s partner enabled on their TheFork account. So, like Formitable and OpenTable, one verification call is enough: a reachable Restaurant ID proves both that the restaurant exists and that the partner is enabled on it. The ID is the UUID TheFork gives the restaurant in TheFork Manager, sent as a string. Until TheFork has approved the platform as a partner and issued the credentials, the verify call answers 503 — see TheFork for what that means today.
Verify a TheFork restaurant
POST /appointments/thefork-restaurants/verify
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The TheFork Restaurant ID, a UUID such as 9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/thefork-restaurants/verify?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" }'
Response (200 OK):
{
"success": true,
"data": {
"verified": true,
"restaurantDetails": {
"restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60",
"partySizes": [1, 2, 3, 4, 5, 6, 7, 8]
}
}
}
partySizes are the party sizes the restaurant takes online over the next 30 days. A 404 or 403 means TheFork would not give us a restaurant with that ID — either the ID is wrong, or the platform’s partner is not enabled on that restaurant yet; a 400 means the ID is not a UUID. Rate-limited to 10 attempts per 5 minutes per account.
Add a TheFork restaurant
POST /appointments/thefork-restaurants
| Field | Required | Description |
|---|---|---|
restaurant_id |
Yes | The verified Restaurant ID (UUID). |
restaurant_name |
Yes | Display name (a label; also what the AI calls the restaurant). |
website_url |
No | An http(s) URL shown to guests when the AI hands them off to the restaurant. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/thefork-restaurants?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "restaurant_id": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60", "restaurant_name": "The Blue Door", "website_url": "https://thebluedoor.example" }'
Response (201 Created): { "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }
Update a saved TheFork restaurant
PUT /appointments/thefork-restaurants/{restaurantId}
Send any of restaurant_name, is_active (pause with false) or website_url (empty string clears it); omitted fields are left unchanged.
curl -X PUT "https://api.youraiconnector.com/v1/appointments/thefork-restaurants/9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK): { "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }
Remove a TheFork restaurant
DELETE /appointments/thefork-restaurants/{restaurantId}
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/thefork-restaurants/9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK): { "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }
A restaurantId not currently on the account returns 404 on update or delete.
Trafft
Trafft is connected once for the whole account, not per location: one company address plus the API credentials from the Trafft admin panel (Features & Integrations → API & Connectors, part of Trafft’s Business plan). The connect call checks those credentials against Trafft before storing anything, so a wrong address, wrong credentials or a plan without API access fails here rather than in a customer conversation. The client secret is stored encrypted and is never returned by any endpoint.
All three write calls (POST, PUT, DELETE) require the Integrations edit permission; the status GET needs Integrations view.
Connect Trafft
POST /appointments/trafft/connect
| Field | Required | Description |
|---|---|---|
subdomain |
Yes | The company address — the part before .admin.trafft.com in the URL you sign in at, e.g. acme. A full address is accepted and reduced to the same value. |
client_id |
Yes | Client ID from Trafft’s API & Connectors page. |
client_secret |
Yes | Client Secret from the same page. Stored encrypted, never returned. |
company_name |
No | A label for your own list. |
curl -X POST "https://api.youraiconnector.com/v1/appointments/trafft/connect?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subdomain": "acme",
"client_id": "YOUR_TRAFFT_CLIENT_ID",
"client_secret": "YOUR_TRAFFT_CLIENT_SECRET",
"company_name": "Acme Salon"
}'
Response (200 OK):
{
"success": true,
"data": {
"subdomain": "acme",
"service_count": 12,
"employee_count": 4,
"location_count": 2
}
}
The counts come back from Trafft during the check — they are the quickest way to confirm the credentials point at the account you meant.
Get the connection status
GET /appointments/trafft
curl "https://api.youraiconnector.com/v1/appointments/trafft" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK):
{
"success": true,
"data": {
"connected": true,
"subdomain": "acme",
"hostname": "acme.admin.trafft.com",
"client_id": "YOUR_TRAFFT_CLIENT_ID",
"company_name": "Acme Salon",
"is_active": true,
"service_count": 12,
"employee_count": 4,
"location_count": 2
}
}
connected: false means nothing is set up yet. The client secret is never included in this response.
Update the connection
PUT /appointments/trafft
| Field | Required | Description |
|---|---|---|
is_active |
No | Set false to pause — the AI stops booking into Trafft, the connection stays. true resumes it. |
company_name |
No | New label. |
curl -X PUT "https://api.youraiconnector.com/v1/appointments/trafft" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Response (200 OK): the same connection object as the GET above.
Disconnect Trafft
DELETE /appointments/trafft
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/trafft" \
-H "X-API-Key: YOUR_API_KEY"
Response (200 OK): { "success": true }
Disconnecting removes the stored credentials only. Appointments already in Trafft are untouched.
Error shape on all Zenchef/Formitable/OpenTable/TheFork endpoints: unlike the rest of this page, errors here carry their status twice — once as the HTTP status and once as
error_codein the body — for example{ "success": false, "error": "Restaurant not found", "error_code": 404 }. Handle it the same way as any other error: checksuccess, readerrorfor the message.
Appointments API errors
Appointment endpoints return the standard error envelope:
{
"success": false,
"error": "Appointment not found"
}
| Status | When it happens on an appointment endpoint |
|---|---|
400 |
A required field is missing or invalid — for example a bad start_time, an end_time not after start_time, an invalid filter combination, no fields to update, or an already-canceled appointment. When booking: neither event_id nor title sent, both sent together, both end_time and duration_minutes sent, a title over 200 characters, or a duration_minutes outside 1–1440. |
404 |
The appointment, contact, or event type was not found. |
409 |
The requested time slot is already taken (booking conflict), or on an update, the external_reference is already used by another appointment. |
The shared codes every endpoint can return — 401, 403 (your plan does not include API access), 429 (rate limit) and 500 — are listed with retry guidance in Errors & Pagination.
Next steps
- Contacts — create and look up the contacts you book for.
- Messages & Conversations — send a contact a confirmation or reminder.
- Webhooks — get notified when appointments change.