Checking your session…

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.

GET/api/v1/apps/{appId}/bookingmanage_bookings
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "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.

GET/api/v1/apps/{appId}/booking/authorize?provider=googlemanage_bookings
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking/authorize' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{ "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):

GET/api/v1/apps/{appId}/bookings?days=7manage_bookings
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/bookings' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "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)

  • types must be an object; each type key must match ^[a-z0-9_]+$, and duplicate keys are rejected.
  • duration and slotAlignment, if present, must be positive numbers.
  • leadTimeMins >= 0; position >= 1; nextAvailableScanDays a whole number 1–70.
  • allowedDays must be an array of whole numbers 0–6.
  • startHour/endHour (and every dayWindows entry) must satisfy 0 <= startHour < endHour <= 23.
  • The Semble new-patient flow requires a newPatientFormUrl unless 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:

  1. 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.
  2. The type's config rules — the assistant then filters those slots by allowedDays (or per-day dayWindows), business hours (startHour/endHour), leadTimeMins, and the nextAvailableScanDays horizon. This is the layer you control here.
  3. 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.


GET/api/v1/apps/{appId}/booking-configmanage_booking_config
curl https://api.everycallhandled.com/api/v1/apps/$APP_ID/booking-config \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "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
}
PUT/api/v1/apps/{appId}/booking-configmanage_booking_config
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
        }
      }
    }
  }'
Sign in to run this against your account.
GET/api/v1/apps/{appId}/booking-config/semble-catalogmanage_booking_config
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/booking-config/semble-catalog' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "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" }
  ]
}
JSON
{ "products": [], "practitioners": [], "locations": [], "error": "Semble integration is not configured for this app." }