Bookings
Answering questions is good; taking bookings is better. Connect your assistant to a calendar and it can offer real availability and book appointments during the conversation — on the phone and in web chat — with the booking landing straight in your diary.
The generic path is simple: connect a calendar (Google today, Microsoft 365 supported), set your booking rules, and the assistant does the rest. Businesses using a supported third-party diary system instead of a calendar are covered by dedicated integrations — see the Semble integration at the end of this chapter for the first of these.
Requires manage_bookings.
Response (200): the app's booking settings plus the connection state:
status is connected or
disconnected. Apps using the Semble integration always
report connected — their diary lives in Semble itself.
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking' \
-H 'Authorization: Bearer YOUR_TOKEN'{
"booking": { /* schedule, appointment types, ... */ },
"connection": { "status": "connected", "connectedEmail": "owner@example.com" }
}Requires manage_bookings. Returns a signed
hosted-consent URL the calendar owner opens in their browser to grant
access. provider is google (default) or
microsoft. Nothing connects until they complete the consent
screen; they land back on the app's Bookings tab afterwards.
Response (200):
{ "configured": false, "url": null } means calendar
booking is not enabled for your account — contact support.
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking/authorize' \
-H 'Authorization: Bearer YOUR_TOKEN'{ "configured": true, "provider": "google", "url": "https://api.eu.nylas.com/v3/connect/auth?..." }Requires manage_bookings. Lists every upcoming
appointment across the app's connected calendar(s) for the next
days days (1-31, default 7) — the business-owner view of
what the assistant has booked. Currently supported for
calendar-connected apps; apps on the Semble integration return
supported: false with a message pointing at the Semble
diary, since their appointments live there.
Response (200):
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/bookings' \
-H 'Authorization: Bearer YOUR_TOKEN'{
"provider": "nylas",
"supported": true,
"windowDays": 7,
"count": 2,
"bookings": [
{ "eventId": "...", "date": "2026-07-18", "time": "10:30", "label": "Check-up", "title": "Mrs Hughes — Check-up" }
]
}What is Semble? Semble is a practice management system widely used by private healthcare clinics in the UK — it holds the clinic's diary, its appointment types and its records. If your business doesn't use Semble, skip this whole section: the generic calendar booking above is the path for you. Semble is the first of a growing set of third-party system integrations; others will follow the same pattern — a special-case configuration surface layered on top of the generic booking experience.
For a Semble-connected app, appointment booking is driven by a declarative booking config — a set of named appointment types, each binding a Semble product/location to the rules the assistant follows when it offers and books slots (practitioner binding, new-patient flow, scan horizon, per-day hours). The config is applied to the live assistant immediately on write.
Every route in this section Requires
manage_booking_config — an opt-in only
permission: it is not part of any role baseline, not even
Owner. It's granted per user with an explicit
+manage_booking_config override from the Team page in your
dashboard. Don't assume an Owner token can hit these endpoints — verify
the grant.
Read the current config
Response (200):
hasSemble tells an editor whether the Semble integration
is wired up for this app (and therefore whether the catalog picker below
will return anything).
Write the config
The body must carry a top-level appointmentBooking
object with a types map. The write replaces only
the appointmentBooking sub-key and preserves every
sibling verbatim; live calls pick the change up immediately.
A successful PUT returns the same shape as the
GET (re-read after write). Omit
appointmentBooking and you get
400 appointmentBooking is required.
The per-type schema
Each entry under types is keyed by a type
key — the machine identifier, which must match
^[a-z0-9_]+$ (e.g. initial_consult). Fields on
the type object:
| Field | Type | Notes |
|---|---|---|
displayName |
string | The label the assistant speaks to the caller. Trimmed on write. |
sembleProductId |
string | The Semble appointment-type/product this maps to. |
locationId |
string | The Semble location the booking is created against. |
practitionerId / practitionerName |
string | Optional Semble practitioner binding. |
duration |
number | Appointment length in minutes. Must be positive. |
slotAlignment |
number | Slot grid granularity in minutes (e.g. 15). Must be positive. |
allowedDays |
int[] | Bookable weekdays, 0=Sun … 6=Sat. Deduped
and sorted on write; each entry must be a whole number 0–6. Empty/absent
= no day restriction. |
leadTimeMins |
number | Minimum notice before a slot can be booked. Must be >= 0. |
nextAvailableScanDays |
int | How many days ahead the assistant scans for the next free slot. Whole number 1–70. |
position |
int | Optional presentation order (see below). Must be >= 1. |
startHour / endHour |
number | Optional default business hours for the type. When either is set,
both must satisfy
0 <= startHour < endHour <= 23. |
dayWindows |
object | Optional per-day hours override,
{ "1": { "startHour": 9, "endHour": 15 } }. Keys 0–6; same
hour rule. Takes precedence over the flat
startHour/endHour, and doubles as the day
whitelist where hours differ by day. |
newPatientFlow |
bool | Marks this as a Semble new-patient type (triggers the intake/questionnaire flow). |
newPatientFormUrl |
string | External questionnaire URL. Required when the Semble new-patient flow is on — unless the app's hosted intake form is active (see Intake Forms), which replaces it. |
Unknown keys on a type are preserved (forward-compat), so a newer field survives a round-trip through an older editor.
Position ordering. position is
1-based (1 = first). It is the
order the assistant offers types in — it feeds the booking tool's
options and the assistant's coaching, and drives card lists in
chat/voice. So "always offer the consultation type first" is
configuration, not model judgement. Types without a
position fall after all positioned types, in
stored-object order. Positioned types sort ascending by
position, ties broken by stored order.
Semble catalog (live pickers)
Returns live Semble products, practitioners, and locations so an editor can render real pickers instead of asking people to hand-type IDs.
This call reaches out to Semble live. If the app has no Semble
credential configured, or Semble can't be reached, you get empty arrays
plus an error string explaining why — the endpoint never
hard-fails:
Validation rules (enforced on PUT)
typesmust be an object; each type key must match^[a-z0-9_]+$, and duplicate keys are rejected.durationandslotAlignment, if present, must be positive numbers.leadTimeMins>= 0;position>= 1;nextAvailableScanDaysa whole number 1–70.allowedDaysmust be an array of whole numbers 0–6.startHour/endHour(and everydayWindowsentry) must satisfy0 <= startHour < endHour <= 23.- The Semble new-patient flow requires a
newPatientFormUrlunless the hosted intake form is active on the app.
All errors are collected and returned together as a 400
with a ;-joined message.
How availability is actually decided
Availability for a Semble-connected app is the product of three independent layers, and the config is only one of them:
- Semble's own schedule — Semble returns the raw set of open slots for the product/practitioner/location. If Semble says a slot doesn't exist, no config can conjure it.
- The type's config rules — the assistant then
filters those slots by
allowedDays(or per-daydayWindows), business hours (startHour/endHour),leadTimeMins, and thenextAvailableScanDayshorizon. This is the layer you control here. - The assistant's prompt — how types are offered,
sequenced (via
position) and spoken about on the call.
A slot is only offered when all three agree. So if a Wednesday slot
never comes up, check in order: does Semble show it open, do
allowedDays/dayWindows include Wednesday
within hours, and does the prompt surface that type at all.
curl https://api.everycallhandled.com/api/v1/apps/$APP_ID/booking-config \
-H 'Authorization: Bearer YOUR_TOKEN'{
"appointmentBooking": {
"types": {
"initial_consult": {
"displayName": "New Patient Consultation",
"sembleProductId": "prod_8812",
"locationId": "loc_04",
"practitionerId": "usr_231",
"practitionerName": "Dr Amara Okafor",
"duration": 30,
"slotAlignment": 15,
"leadTimeMins": 120,
"nextAvailableScanDays": 21,
"allowedDays": [1, 3, 5],
"startHour": 9,
"endHour": 17,
"position": 1,
"newPatientFlow": true,
"newPatientFormUrl": "https://forms.gle/zTEZEZv9example"
},
"follow_up": {
"displayName": "Follow-up",
"sembleProductId": "prod_8815",
"locationId": "loc_04",
"duration": 15,
"slotAlignment": 15,
"leadTimeMins": 60,
"nextAvailableScanDays": 14,
"allowedDays": [1, 2, 3, 4, 5],
"position": 2
}
}
},
"hasSemble": true,
"editable": true
}curl -X PUT https://api.everycallhandled.com/api/v1/apps/$APP_ID/booking-config \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"appointmentBooking": {
"types": {
"initial_consult": {
"displayName": "New Patient Consultation",
"sembleProductId": "prod_8812",
"locationId": "loc_04",
"duration": 30,
"slotAlignment": 15,
"leadTimeMins": 120,
"nextAvailableScanDays": 21,
"allowedDays": [1, 3, 5],
"position": 1
}
}
}
}'curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking-config/semble-catalog' \
-H 'Authorization: Bearer YOUR_TOKEN'{
"products": [
{ "id": "prod_8812", "name": "New Patient Consultation", "duration": 30, "locationId": null }
],
"practitioners": [
{ "id": "usr_231", "name": "Dr Amara Okafor" }
],
"locations": [
{ "id": "loc_04", "name": "Harley Street — Main" }
]
}{ "products": [], "practitioners": [], "locations": [], "error": "Semble integration is not configured for this app." }