For developers
The clinic API
One key per clinic, made by the clinic under Settings → API keys, and every answer scoped to that clinic. Read patients, appointments, practitioners, therapies, locations and invoices; create and update patients and record attendance; find times, hold and book, move and cancel, book classes; and receive webhooks. The same document as JSON: /api/agent/v1/openapi.json.
Authentication
Send the key as a bearer token on every request. A key is shown once when made and can be revoked at any time; each key is limited to 120 requests a minute.
curl https://www.theclinicdiary.com/api/agent/v1/clinic \
-H "Authorization: Bearer tcd_…"
Answers and errors
JSON in, JSON out. A refused request answers 400 with an error, and where the agent might read it out, a spoken line; 401 is a bad key, 403 a capability the key may not use, 404 a thing that is not this clinic's. Dates are ISO 8601; times carry an offset; money is pence.
Reading
The administrative record. Nothing clinical is ever returned: no notes, documents, outcomes or contraindications.
GET/api/agent/v1/patientslist_patients
Search patients
The administrative record of every active patient, or those matching a search. Paged.
Parameters
| search | query · string | Part of a name, mobile, email or postcode. |
| page* | query · integer | |
| pageSize* | query · integer | |
Answer
| patients* | object[] | | id* | uuid | | | title* | string | null | | | firstName* | string | | | lastName* | string | | | dateOfBirth* | string | null | | | gender* | string | null | | | mobilePhone* | string | null | | | homePhone* | string | null | | | email* | string | null | | | addressLine1* | string | null | | | addressLine2* | string | null | | | cityCounty* | string | null | | | postcode* | string | null | | | gpName* | string | null | | | gpPractice* | string | null | | | consentText* | boolean | | | consentEmail* | boolean | | | createdAt* | string | null | | | updatedAt* | string | null | |
|
| page* | integer | |
| hasMore* | boolean | |
GET/api/agent/v1/patients/{patientId}get_patient
A patient
One patient's administrative record and upcoming appointments. Viewing is on the patient's audit trail.
Answer
| patient* | object | The administrative record. Nothing clinical is ever returned.| id* | uuid | | | title* | string | null | | | firstName* | string | | | lastName* | string | | | dateOfBirth* | string | null | | | gender* | string | null | | | mobilePhone* | string | null | | | homePhone* | string | null | | | email* | string | null | | | addressLine1* | string | null | | | addressLine2* | string | null | | | cityCounty* | string | null | | | postcode* | string | null | | | gpName* | string | null | | | gpPractice* | string | null | | | consentText* | boolean | | | consentEmail* | boolean | | | createdAt* | string | null | | | updatedAt* | string | null | |
|
| upcoming* | object[] | | id* | uuid | | | startISO* | date-time | | | endISO* | date-time | | | spoken* | string | | | status* | string | | | therapy* | string | null | | | therapyId* | uuid | | | practitioner* | string | null | | | practitionerId* | uuid | |
|
GET/api/agent/v1/appointmentslist_appointments
Appointments by date
Appointments in a date range, optionally for one practitioner or patient.
Parameters
| from* | query · string | First day, inclusive. |
| to* | query · string | Last day, inclusive. At most three months after from. |
| practitionerId | query · uuid | |
| patientId | query · uuid | |
| status* | query · "active" | "cancelled" | "all" | |
Answer
| appointments* | object[] | | id* | uuid | | | patientId* | uuid | | | patientName* | string | null | | | practitionerId* | uuid | | | practitioner* | string | null | | | therapyId* | uuid | | | therapy* | string | null | | | startISO* | date-time | | | endISO* | date-time | | | status* | string | | | didNotAttend* | boolean | | | checkedInISO* | string | null | | | bookedOnline* | boolean | | | isTelehealth* | boolean | | | cancelledReason* | string | null | |
|
GET/api/agent/v1/appointments/{appointmentId}get_appointment
An appointment
One appointment.
Path
| appointmentId* | path · uuid | |
Answer
| id* | uuid | |
| patientId* | uuid | |
| patientName* | string | null | |
| practitionerId* | uuid | |
| practitioner* | string | null | |
| therapyId* | uuid | |
| therapy* | string | null | |
| startISO* | date-time | |
| endISO* | date-time | |
| status* | string | |
| didNotAttend* | boolean | |
| checkedInISO* | string | null | |
| bookedOnline* | boolean | |
| isTelehealth* | boolean | |
| cancelledReason* | string | null | |
GET/api/agent/v1/practitionerslist_practitioners
Practitioners
Active practitioners.
GET/api/agent/v1/therapieslist_therapies
Therapies
Active therapies with lengths, prices, whether they are bookable online, and who offers them.
Answer
| therapies* | object[] | | id* | uuid | | | name* | string | | | newPatientMinutes* | integer | null | | | newPatientPrice* | string | null | | | followUpMinutes* | integer | null | | | followUpPrice* | string | null | | | bookableOnline* | boolean | | | practitionerIds* | uuid[] | |
|
GET/api/agent/v1/locationslist_locations
Locations
The clinic's sites.
Answer
| locations* | object[] | | id* | uuid | | | name* | string | | | address* | string | null | | | postcode* | string | null | | | phoneNumber* | string | null | | | isPrimary* | boolean | |
|
GET/api/agent/v1/invoiceslist_invoices
Invoices
Invoices and credit notes, filtered by patient, status or issue date. Paged.
Parameters
| patientId | query · uuid | |
| status | query · "unpaid" | "paid" | "part_paid" | "void" | "refunded" | "draft" | |
| from | query · string | Issued on or after. |
| to | query · string | Issued on or before. |
| page* | query · integer | |
| pageSize* | query · integer | |
Answer
| invoices* | object[] | | id* | uuid | | | patientId* | uuid | | | type* | string | | | status* | string | | | invoiceNumber* | integer | null | | | source* | string | | | subtotalPence* | integer | | | discountPence* | integer | | | taxPence* | integer | | | totalPence* | integer | | | paymentMethod* | string | null | | | issuedISO* | date-time | | | paidISO* | string | null | |
|
| page* | integer | |
| hasMore* | boolean | |
GET/api/agent/v1/invoices/{invoiceId}get_invoice
An invoice
One invoice with its lines.
Answer
| invoice* | object | | id* | uuid | | | patientId* | uuid | | | type* | string | | | status* | string | | | invoiceNumber* | integer | null | | | source* | string | | | subtotalPence* | integer | | | discountPence* | integer | | | taxPence* | integer | | | totalPence* | integer | | | paymentMethod* | string | null | | | issuedISO* | date-time | | | paidISO* | string | null | |
|
| lines* | object[] | | id* | uuid | | | description* | string | | | quantity* | number | | | unitPricePence* | integer | | | totalPence* | integer | |
|
Writing
Writes go through the same code the desk uses, so consent records, task rules and webhooks fire the same way.
POST/api/agent/v1/patientscreate_patient
Create a patient
A new patient record with the opening consent position recorded. Refuses an apparent duplicate unless told otherwise.
Body
| title | string | null | |
| firstName* | string | |
| lastName* | string | |
| dateOfBirth | string | null | |
| gender | string | null | |
| mobilePhone | string | null | |
| homePhone | string | null | |
| email | string | null | |
| addressLine1 | string | null | |
| addressLine2 | string | null | |
| cityCounty | string | null | |
| postcode | string | null | |
| gpName | string | null | |
| gpPractice | string | null | |
| consentText | boolean | Service texts (reminders). Defaults to true. |
| consentEmail | boolean | Service emails. Defaults to true. |
| allowDuplicate | boolean | Create even if a patient with the same name and contact exists. |
Answer
| patient* | object | The administrative record. Nothing clinical is ever returned.| id* | uuid | | | title* | string | null | | | firstName* | string | | | lastName* | string | | | dateOfBirth* | string | null | | | gender* | string | null | | | mobilePhone* | string | null | | | homePhone* | string | null | | | email* | string | null | | | addressLine1* | string | null | | | addressLine2* | string | null | | | cityCounty* | string | null | | | postcode* | string | null | | | gpName* | string | null | | | gpPractice* | string | null | | | consentText* | boolean | | | consentEmail* | boolean | | | createdAt* | string | null | | | updatedAt* | string | null | |
|
| upcoming* | object[] | | id* | uuid | | | startISO* | date-time | | | endISO* | date-time | | | spoken* | string | | | status* | string | | | therapy* | string | null | | | therapyId* | uuid | | | practitioner* | string | null | | | practitionerId* | uuid | |
|
PATCH/api/agent/v1/patients/{patientId}update_patient
Update a patient
Change administrative details. Only the fields sent are changed.
Body
| title | string | null | |
| firstName | string | |
| lastName | string | |
| dateOfBirth | string | null | |
| gender | string | null | |
| mobilePhone | string | null | |
| homePhone | string | null | |
| email | string | null | |
| addressLine1 | string | null | |
| addressLine2 | string | null | |
| cityCounty | string | null | |
| postcode | string | null | |
| gpName | string | null | |
| gpPractice | string | null | |
Answer
| patient* | object | The administrative record. Nothing clinical is ever returned.| id* | uuid | | | title* | string | null | | | firstName* | string | | | lastName* | string | | | dateOfBirth* | string | null | | | gender* | string | null | | | mobilePhone* | string | null | | | homePhone* | string | null | | | email* | string | null | | | addressLine1* | string | null | | | addressLine2* | string | null | | | cityCounty* | string | null | | | postcode* | string | null | | | gpName* | string | null | | | gpPractice* | string | null | | | consentText* | boolean | | | consentEmail* | boolean | | | createdAt* | string | null | | | updatedAt* | string | null | |
|
| upcoming* | object[] | | id* | uuid | | | startISO* | date-time | | | endISO* | date-time | | | spoken* | string | | | status* | string | | | therapy* | string | null | | | therapyId* | uuid | | | practitioner* | string | null | | | practitionerId* | uuid | |
|
POST/api/agent/v1/appointments/{appointmentId}/attendancerecord_attendance
Record attendance
Mark an appointment attended or did-not-attend once it has started. Fires the same follow-ups the desk's tick does.
Path
| appointmentId* | path · uuid | |
Body
| status* | "attended" | "did_not_attend" | |
Answer
| id* | uuid | |
| patientId* | uuid | |
| patientName* | string | null | |
| practitionerId* | uuid | |
| practitioner* | string | null | |
| therapyId* | uuid | |
| therapy* | string | null | |
| startISO* | date-time | |
| endISO* | date-time | |
| status* | string | |
| didNotAttend* | boolean | |
| checkedInISO* | string | null | |
| bookedOnline* | boolean | |
| isTelehealth* | boolean | |
| cancelledReason* | string | null | |
Booking, changing and handing over
The endpoints the AI call agent uses. They honour the clinic's online-booking rules: bookable therapies, notice period, payment modes and the fields a new patient must give. A change is read back first and carried out on a second call with confirm: true.
POST/api/agent/v1/calleridentify_caller
Identify the caller
Look the caller up by the number they are calling from, and confirm who they are from their name and date of birth. Never reveals details until confirmed.
Body
| phone | string | The number the caller is ringing from, in any format. |
| firstName | string | |
| lastName | string | |
| dateOfBirth | string | YYYY-MM-DD, as the caller states it. |
Answer
| known* | boolean | Whether the phone number matches a patient at this clinic. |
| greetAs* | string | null | A first name to greet with, when the number is known. Not confirmation of identity. |
| confirmed* | boolean | True when the name and date of birth given match a patient. |
| patientId* | uuid | |
| firstName* | string | null | |
| lastName* | string | null | |
| upcoming* | object[] | | appointmentId* | uuid | | | startISO* | date-time | | | spoken* | string | The date and time in words, in the clinic's time zone. | | therapy* | string | null | | | practitioner* | string | null | | | location* | string | null | |
|
| note* | string | null | What to say next when identity could not be confirmed. |
GET/api/agent/v1/clinicclinic_information
About the clinic
Name, sites, opening hours, what is offered with prices, booking and cancellation rules, and what a new patient must supply.
Answer
| name* | string | |
| timezone* | string | |
| phone* | string | null | |
| locations* | object[] | | id* | uuid | | | name* | string | | | address* | string | null | | | arrivalInfo* | string | null | | | hours* | object[] | | day* | string | | | open* | string | | | close* | string | |
|
|
| openNow* | boolean | |
| hoursToday* | object[] | |
| therapies* | object[] | Only what the clinic offers for booking over the phone.| id* | uuid | | | name* | string | | | description* | string | null | | | newPatientMinutes* | integer | | | newPatientPrice* | string | null | | | followUpMinutes* | integer | null | | | followUpPrice* | string | null | | | practitioners* | object[] | |
|
| classes* | boolean | Whether the clinic runs classes the agent may book. |
| bookingNoticeHours* | integer | |
| bookingWindowDays* | integer | |
| cancellationHours* | integer | |
| cancellationPolicy* | string | null | |
| paymentNewPatient* | "none" | "deposit" | "full" | |
| paymentExistingPatient* | "none" | "deposit" | "full" | |
| depositPrice* | string | null | |
| newPatientFields* | object[] | What a new patient must supply before they can be booked.| key* | string | | | label* | string | | | type* | string | |
|
| capabilities* | object | | information* | boolean | | | book* | boolean | | | amend* | boolean | | | newPatients* | boolean | | | classes* | boolean | | | payment* | boolean | | | handover* | boolean | | | transfer* | boolean | |
|
| instructions* | string | null | Anything the clinic wants said or done on every call. |
| callbackPromise* | string | null | |
POST/api/agent/v1/availabilityfind_appointments
Free appointment times
Times free for a therapy over the next days, honouring the clinic's notice period, booking window and online-booking filters.
Body
| therapyId* | uuid | |
| practitionerId | uuid | Leave out to search every practitioner who offers the therapy. |
| patientType* | "new" | "existing" | (default "new") |
| fromDate | string | First day to look at; today when omitted. |
| days* | integer | (default 7) |
| partOfDay* | "morning" | "afternoon" | "evening" | "any" | (default "any") |
Answer
| therapy* | string | |
| durationMinutes* | integer | |
| days* | object[] | | date* | string | | | spoken* | string | | | slots* | object[] | | startISO* | date-time | | | durationMinutes* | integer | | | spoken* | string | | | practitionerId* | uuid | | | practitioner* | string | | | locationId* | uuid | | | location* | string | null | |
|
|
| nextAfter* | string | null | When nothing suits, the first day after this range with anything free. |
POST/api/agent/v1/holdshold_appointment
Hold a time
Hold a time for thirty minutes while the caller decides or pays. Book against the hold with /bookings.
Body
| therapyId* | uuid | |
| practitionerId* | uuid | |
| startISO* | date-time | |
| patientType* | "new" | "existing" | (default "new") |
Answer
| holdToken* | string | |
| expiresISO* | date-time | |
| spoken* | string | |
POST/api/agent/v1/bookingsbook_appointment
Book a held time
Book the held time for an existing patient or a new one. If a payment is required the caller is texted a link and the booking is confirmed when they pay; it lapses after thirty minutes.
Body
| holdToken* | string | |
| therapyId* | uuid | |
| patientId | uuid | An existing patient, confirmed through /caller. |
| newPatient | object | A new patient. Every field the clinic marks mandatory must be present.| firstName* | string | | | lastName* | string | | | mobile* | string | | | email* | string | | | dateOfBirth | string | | | addressLine1 | string | | | addressLine2 | string | | | cityCounty | string | | | postcode | string | | | gpName | string | | | gpPractice | string | | | fields | object | Answers to the clinic's own mandatory fields, keyed as newPatientFields gives them. |
|
| notes | string | Anything the caller asked to pass on about the visit. |
Answer
| status* | "confirmed" | "awaiting_payment" | |
| appointmentId* | uuid | |
| patientId* | uuid | |
| spoken* | string | The booking read back in words. |
| payment* | object | null | | amount* | string | | | kind* | "deposit" | "full" | | | sentTo* | string | | | expiresISO* | date-time | | | spoken* | string | |
|
| missingFields* | string[] | When the booking was refused for a new patient, the labels still needed. |
POST/api/agent/v1/bookings/{appointmentId}/cancelcancel_appointment
Cancel an appointment
First call returns whether it is a late cancellation and any charge, to read out. Call again with confirm true to cancel.
Path
| appointmentId* | path · uuid | |
Body
| patientId* | uuid | |
| confirm* | boolean | False returns the consequences to read out; true carries the change out. (default false) |
Answer
| done* | boolean | |
| late* | boolean | |
| charge* | string | null | The late-cancellation charge that applies, in words, or null. |
| policy* | string | null | |
| spoken* | string | |
POST/api/agent/v1/bookings/{appointmentId}/reschedulemove_appointment
Move an appointment
Move an appointment to a free time found with /availability for the same practitioner. First call reads back; confirm true moves it.
Path
| appointmentId* | path · uuid | |
Body
| patientId* | uuid | |
| confirm* | boolean | False returns the consequences to read out; true carries the change out. (default false) |
| startISO* | date-time | |
Answer
| done* | boolean | |
| late* | boolean | |
| policy* | string | null | |
| from* | string | |
| to* | string | |
| spoken* | string | |
POST/api/agent/v1/classes/availabilityfind_classes
Classes with places
Upcoming classes with places left and their prices.
Body
| fromDate | string | |
| days* | integer | (default 14) |
| courseId | uuid | |
Answer
| sessions* | object[] | | sessionId* | uuid | | | courseId* | uuid | | | course* | string | | | description* | string | null | | | startISO* | date-time | | | spoken* | string | | | practitioner* | string | null | | | placesLeft* | integer | | | price* | string | null | | | wholeCoursePrice* | string | null | | | remainingInCourse* | integer | |
|
POST/api/agent/v1/classes/enrolbook_class
Book a class place
Book a confirmed patient onto a class, or onto this and every later class in the course. A class with a fee is paid by text link, as an appointment is.
Body
| sessionId* | uuid | |
| patientId* | uuid | |
| scope* | "session" | "remaining" | This class only, or this and every later class in the course. (default "session") |
Answer
| status* | "confirmed" | "awaiting_payment" | |
| enrolmentId* | uuid | |
| classes* | integer | |
| spoken* | string | |
| payment* | object | null | | amount* | string | | | sentTo* | string | | | expiresISO* | date-time | | | spoken* | string | |
|
POST/api/agent/v1/handoverhand_over_to_clinic
Hand over to a person
When the caller wants a person or the call is beyond the agent. Transfers to the clinic if it has a number and somebody answers; otherwise takes a message for the clinic.
Body
| reason* | "caller_asked" | "too_complex" | "identity_failed" | "complaint" | "clinical" | "other" | |
| wantsPerson* | boolean | True when the caller wants to speak to somebody now; the clinic's transfer number is tried first if it has one. (default false) |
| callerName | string | |
| callerPhone | string | |
| patientId | uuid | |
| message | string | The message to leave, in the caller's words. |
| summary | string | What the call was about so far. |
Answer
| action* | "transferring" | "message_taken" | "take_message" | |
| spoken* | string | What to tell the caller. |
Webhooks
Register an https URL and be told when something happens, with a signed payload and retries.
GET/api/agent/v1/webhookslist_webhooks
Webhooks
The clinic's webhooks and the events available.
Answer
| webhooks* | object[] | | id* | uuid | | | url* | string | | | events* | string[] | | | description* | string | null | | | isActive* | boolean | | | lastDeliveredISO* | string | null | | | lastFailedISO* | string | null | | | failures* | integer | | | createdISO* | date-time | |
|
| events* | string[] | Every event name that can be subscribed to. |
POST/api/agent/v1/webhookscreate_webhook
Create a webhook
Register an https URL for events. The signing secret is returned once.
Body
| url* | string | |
| events* | string[] | Event names; empty subscribes to everything. (default []) |
| description | string | |
Answer
| id* | uuid | |
| url* | string | |
| events* | string[] | |
| secret* | string | Shown once. Sign-check every delivery with it. |
DELETE/api/agent/v1/webhooks/{webhookId}delete_webhook
Delete a webhook
Stops deliveries at once.
Receiving webhooks
Each delivery is a POST with a JSON body { id, event, occurredAt, clinicId, data } and three headers: x-clinicdiary-event, x-clinicdiary-delivery, and x-clinicdiary-signature as t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of `${t}.${body}` with the secret shown when the webhook was made. Answer 2xx within ten seconds; anything else is retried after 1, 5 and 30 minutes, then 2 and 12 hours, then given up. Deliveries and their outcomes are listed under Settings → API keys.
Events
appointment.booked — An appointment is bookedappointment.moved — An appointment is movedappointment.cancelled — An appointment is cancelledappointment.attendance — Attendance is recorded (attended or did not attend)patient.created — A patient is createdinvoice.paid — An invoice is paid
Verifying a signature (Node)
const [t, v1] = req.headers['x-clinicdiary-signature']
.split(',').map((p) => p.split('=')[1]);
const expected = crypto.createHmac('sha256', SECRET)
.update(`${t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
const ok = fresh && crypto.timingSafeEqual(
Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));The AI call agent uses the booking group above with a key of its own; that key cannot reach the reading, writing or webhook endpoints.