Developers
The Daobook API
Book into a Daobook clinic from the clinic's own website or app, or connect an assistant to its diary.
The Daobook API lets a clinic's own software, and an assistant a practitioner uses, work with Daobook: read what is bookable online and when, make, move and cancel bookings under the clinic's own online-booking rules, read the diary and the client list, and hear about changes. Daobook stays the one diary. This is version 1, and its base URL is https://api.daobook.com.au/v1.
Quick start
- Get access. The API is on for every Daobook business. The account owner accepts the API terms of use once, under Settings → Integrations → Developer API.
- Make a key. The account owner opens Settings → Integrations → Developer API in Daobook, names the key, ticks what it may do (read availability, book appointments) and, for a business with several locations, which locations it reaches. The key is shown once and starts with
dbk_live_. Keep it on your server. - Make the first call. Any live key may call
/account; it needs no scope.
curl https://api.daobook.com.au/v1/account \
-H "Authorization: Bearer dbk_live_…"
{ "id": "acct_…", "object": "account", "name": "Willow Lane Chinese Medicine",
"key_id": "key_…", "key_name": "Clinic website",
"scopes": ["availability:read", "appointments:book"], "location_ids": ["loc_…"] }
- Read what can be booked:
GET /v1/locations, thenGET /v1/appointment_types?location_id=…andGET /v1/practitioners?location_id=…. - Read free times:
GET /v1/availability?location_id=…&appointment_type_id=…&from=2026-11-02&to=2026-11-08. - Book:
POST /v1/appointmentswith anIdempotency-Keyheader. The full request is under Booking.
Every endpoint, field, error and event is in the reference, generated from the OpenAPI document, which also generates a client in any language.
Authentication
- Every request carries
Authorization: Bearer dbk_live_…. No other way of sending a key is accepted: not a query parameter, not a cookie, not another header. (An app acting as one person carries an access token,dbk_access_…, instead: Acting as a person.) - A key belongs to the business and is made by the account owner. It holds scopes:
availability:read(locations, practitioners, appointment types and free times),appointments:book(make, read, move and cancel the bookings this business made through the API, and read the event feed),waiting_list:writeandcallers:verify(the sections below),appointments:read(read every appointment in the diary, whoever made it; the Diary section), andclients:readandclients:write(who the clients are, and adding or changing them; the Clients section).GET /v1/accountis the one call that needs no scope. - A key may be narrowed to some of a business's locations. Anything outside them, or belonging to another business, is a
404: the API never confirms that something exists. - Server-side only. The API sends no CORS headers, so a browser cannot call it, and a key built into a web page or into an app that runs on a patient's device can be extracted. If that has happened, revoke the key in Settings; everything using it stops at once.
- To rotate a key, make a second one, switch your software over, then revoke the first. A business may hold up to 10 live keys.
- A revoked, expired or unknown key is
401 invalid_api_key. A key whose business has had the API switched off is403 api_not_enabled; one whose subscription is not active is403 account_inactive; one without the scope an endpoint needs is403 insufficient_scope, naming the scope indetails.required_scope. - Send a
User-Agentthat names your integration and a contact address, such asClinicSite/1.2 (dev@example.com). It goes in the request log and helps us help you.
Acting as a person: OAuth
A key is the business's: it acts for the clinic, with no person behind it. An app a practitioner uses themselves (an AI assistant such as Claude or Claude Code, a desktop tool) connects as that person with OAuth 2.1, does only what their role allows at their locations, and is named with them in every log. Daobook is the authorization server and the resource server; everything an MCP client needs to find its way in is discoverable.
- Discovery.
GET https://api.daobook.com.au/.well-known/oauth-authorization-server(RFC 8414) names the endpoints below and what they support;GET https://api.daobook.com.au/.well-known/oauth-protected-resource(RFC 9728) names the API as a resource. Every401the API answers carriesWWW-Authenticate: Bearer resource_metadata="https://api.daobook.com.au/.well-known/oauth-protected-resource", and a scope refusal names the scope. - Your client. Register it with
POST https://api.daobook.com.au/oauth/register(RFC 7591): a JSON body withclient_nameandredirect_uris(https, orhttp://localhostandhttp://127.0.0.1with any port),token_endpoint_auth_methodabsent or"none"; the answer is aclient_id(app_…) and your metadata as read, never a secret. A registration unused for 30 days is forgotten. Or use a Client ID Metadata Document: the https URL of a JSON document that names itself asclient_idis your client id, read when the person is first sent to Daobook and once a day after. Only public clients register themselves; an app that serves many clinics and holds a secret is registered by us, so write to us. - Authorize. Send the person to
https://www.daobook.com.au/oauth/authorizewithresponse_type=code,client_id,redirect_uri,scope,state,code_challengeandcode_challenge_method=S256, and, if you will also use the MCP server,resource(https://api.daobook.com.auorhttps://api.daobook.com.au/mcp). They sign in and see what you ask for in Daobook's own words with the locations their role allows; they may narrow the locations. Someone who signed in more than ten minutes before confirms it's them on that screen, above Allow, with their passkey, a code from their authenticator app or their password. Allow sends them toredirect_uri?code=…&state=…; Cancel sendserror=access_denied. The screen is shown every time: keep the refresh token rather than asking again. An unknown client or an unregistered redirect gets a page, never a redirect; anything else wrong comes back to you aserroranderror_descriptionwith yourstate. - Scopes a person may grant:
availability:read(any member);appointments:read,clients:readandclients:write(owner, admin, practitioner, receptionist);appointments:bookandwaiting_list:write(owner, admin, receptionist, who manage the calendar);offline_accessfor a refresh token.callers:verifyis never a person's. A scope the person cannot hold anywhere is left out andscopein the token answer says what was actually granted. - Token.
POST https://api.daobook.com.au/oauth/token, form-encoded (JSON accepted):grant_type=authorization_code,code,code_verifier,redirect_uri,client_id(a confidential app sends its secret by HTTP Basic or asclient_secret). The answer isaccess_token(dbk_access_…, one hour),token_type: "Bearer",expires_in: 3600,refresh_token(dbk_refresh_…) andscope.grant_type=refresh_tokenwithrefresh_tokenandclient_idanswers a new pair and retires the old refresh token; a retired refresh token presented again revokes every token of that connection and answersinvalid_grant, so the person signs in again. A refresh token lives 90 days unused and a year at most. Errors are OAuth's own,{ "error", "error_description" }:invalid_request,invalid_client(401),invalid_grant,unsupported_grant_type,invalid_scope,invalid_target. - Revoke.
POST https://api.daobook.com.au/oauth/revokewithtoken(RFC 7009), always200; revoking the connection's last refresh token ends it. - Using the token.
Authorization: Bearer dbk_access_…on any/v1call.GET /v1/accountanswerspractitioner_id,app_idandapp_namebeside the business (key_idisnull). What the token reaches is decided at each request: the locations where the person's current membership is active and their role allows the scope the endpoint needs, so a role change or a departure takes effect at once. A revoked, expired or unknown token is401 invalid_api_key. A change made through your app reads as the person in every history; the request log names your app; each client record it reads appears on that record's Who Opened This Record as the person, through your app. The person sees and ends the connection under Settings → Security → Connected apps; an owner or admin may end any member's. Underappointments:bookthe app changes the diary as the person would in the calendar: any upcoming client booking at the locations their role manages, whoever made it (the Booking section says how a move, a cancel and a confirm differ from a key's). - Limits. The OAuth endpoints are limited per address: thirty token and revocation requests a minute, ten registrations an hour. On
/v1a token has a key's limits. - The MCP server.
https://api.daobook.com.au/mcpis a stateless Model Context Protocol server over Streamable HTTP, answering JSON: add it to Claude, Claude Code, ChatGPT or any MCP client as a remote server and the client finds its way in from/.well-known/oauth-protected-resource/mcpand the401above, through the consent screen, as that person. A business's own agent connects with a key as the bearer instead. Its tools are the operations on this page, offered by the scopes the credential holds (an assistant that may only read the diary sees only reads), each one the documented request made under the same credential, logged and limited as any request; no tool reaches anything clinical. The authorization response carriesiss(RFC 9207) for clients that check it. A practitioner's steps for Claude and Claude Code, and the tools, are under Connecting an assistant.
Connecting an assistant (MCP)
A practitioner can connect an AI assistant they already use to their Daobook, and ask it in their own words to find a free time, book, move or confirm an appointment, put someone on the waiting list, or look up a client's contact details. Claude and Claude Code connect today; so does any assistant that connects to remote MCP servers and signs in with OAuth. The address is:
https://api.daobook.com.au/mcp
The assistant connects as the person: they sign in to Daobook and allow it on Daobook's own screen, which says what it asks for and where. It does only what their role allows, at their locations, and everything it does is logged as them, through the assistant; each client record it reads is shown on that record. Nothing clinical is reachable: no notes, prescriptions, files or form answers. Before anyone at a business can connect one, the account owner accepts the API terms once (Quick start). The person ends the connection under Settings → Security → Connected apps.
Claude (claude.ai or the desktop app)
- Open Customize → Connectors and click Add custom connector. On a Team or Enterprise plan an Owner adds it for everyone under Organization settings → Connectors, Add, then Custom, and each member clicks Connect on it.
- Enter the address above. If Claude asks how it should identify itself (OAuth client), choose Use Claude's published identity. Click Add.
- Click Connect. Daobook opens: sign in, and click Allow on the screen that says Claude wants to connect to your Daobook.
- In a chat, turn Daobook on from + → Connectors and ask, for example, "What's free for a follow-up on Thursday afternoon?" or "Move my 10 o'clock on Friday to the same time next week".
Claude Code
claude mcp add --transport http daobook https://api.daobook.com.au/mcp
Then, in a Claude Code session, type /mcp, choose daobook and Authenticate. Your browser opens Daobook: sign in and click Allow.
A business's own agent connects with a key in place of a person, sending Authorization: Bearer dbk_live_… with every request (in Claude Code, --header "Authorization: Bearer dbk_live_…" on the command above). It is offered the tools its key's scopes allow, at the key's locations.
The tools. Each one is the documented request on this page, made under the same credential and logged and limited as that request is. An assistant is offered only the tools whose scope its connection holds: one allowed to read the diary sees only reads. Each tool tells the assistant whether it only reads or changes something, and Claude asks the person before running one that changes something, as its own permission settings say.
| Tool | What it does | Scope |
|---|---|---|
get_account | The business, the person, the app, the scopes held and the locations reached | none |
list_locations, list_practitioners, list_appointment_types | What can be booked online | availability:read |
find_free_times | Free times for an appointment type, a week at a time | availability:read |
list_appointments, get_appointment | The diary; under appointments:book, also the bookings the person may change | appointments:read or appointments:book |
book_appointment | Book under the online-booking rules | appointments:book |
reschedule_appointment, cancel_appointment, confirm_appointment | Move, cancel or confirm a booking (changes) | appointments:book |
add_to_waiting_list, list_waiting_list_entries, get_waiting_list_entry, remove_from_waiting_list | Put someone on the waiting list, and read or remove the entries it added (removing one changes) | waiting_list:write |
find_client, get_client | Find a client by name, email, mobile or reference, and read their contact details | clients:read |
create_client, update_client | Add a client (a duplicate is refused) or change their contact details (changes) | clients:write |
Conventions
- HTTPS, JSON, UTF-8. A request body needs
Content-Type: application/jsonand is at most 1 MB. - Ids are opaque strings of up to 64 characters: a type prefix, an underscore and 32 hex characters (
loc_41aa…,appt_3f2b…). Store them as strings and never parse or compare them for order. - Every field is always present; an empty one is
null. New fields, enumerated values, event types, error codes and endpoints may appear at any time within version 1: ignore what you do not know. - Unknown parameters are refused (
400 unknown_parameter), so a typo fails loudly. - Times. Diary times (
starts_at,ends_at, a slot's times) are RFC 3339 instants in the location's own time zone, with its offset:2026-11-03T10:00:00+11:00. The location carries its IANA zone name intime_zone. A time you send must carry an offset (any offset will do); one without is refused (400 invalid_timestamp). Record stamps (created_at,updated_at,cancelled_at, an event'stimestamp) are UTC and end inZ. Dates areYYYY-MM-DD, and a date range means whole days in the location's time zone. - Money is
{ "amount": 9500, "currency": "AUD" }: an integer in the currency's smallest unit and an ISO 4217 code. - Lists are
{ "object": "list", "data": [...], "has_more": true, "next_cursor": "…" }, in a fixed order each list documents, paged withlimit(default 50, at most 100) and an opaquecursor. To continue, sendcursorwith the same filters. There are no totals. - Named actions. A change that carries a business rule is a
POSTto a named action (/appointments/{id}/cancel,/appointments/{id}/reschedule), never a field edit. - Safe retries.
Idempotency-Key(any string up to 255 characters; a UUID is a good one) is required onPOST /appointmentsand accepted on everyPOST. The same key with the same body replays the first answer, markedIdempotent-Replayed: true; with a different body it is422 idempotency_key_reused; while the first is still running,409 request_in_progress. A5xxor a429is never stored, so retrying after a fault runs the request again. Keys are kept for 24 hours. - Rate limits. 200 requests a minute per key, of which 30 may be writes. Over it,
429 rate_limitedwithRetry-Afterin seconds. - Request ids. Every response carries
X-Request-Id, and every error body repeats it asrequest_id. Quote it when you write to us. - Errors are one envelope, described under Errors.
Booking
The API books through the same service as the clinic's Daobook booking page, under the same switches: a location, practitioner or appointment type the clinic has not offered for online booking is not listed and cannot be booked (422 not_bookable).
- Locations.
GET /v1/locationslists the key's locations offered for online booking. Each carries itstime_zone, itsbooking_page_url, therequired_client_fieldsthe clinic's booking form insists on, andtermsandprivacy_policy, each with whether it isrequired, the currentversionand thetextto show the client. - Appointment types and practitioners.
GET /v1/appointment_types?location_id=…(optionally&practitioner_id=…) andGET /v1/practitioners?location_id=…. A type carriesduration_minutes,price,practitioner_idsandpayment_required_at_booking; a practitioner carriesonline_booking.minimum_notice_hoursandonline_booking.window_days. - Free times.
GET /v1/availability?location_id=…&appointment_type_id=…&from=…&to=…returns the slots the booking page would offer over a range of at most 31 days, with every rule already applied. Addpractitioner_idfor one practitioner; without it the answer is "any practitioner", and each slot names the one it would book. A slot is a statement about now, not a hold. A time the clinic has offered to its waiting list is held for those people for up to two hours and is left out until the hold ends. - Book.
POST /v1/appointments
Authorization: Bearer dbk_live_…
Idempotency-Key: 6f1c0d2e-7b8a-4c3d-9e0f-1a2b3c4d5e6f
Content-Type: application/json
{
"location_id": "loc_41aa…",
"practitioner_id": "prac_77d0…",
"appointment_type_id": "apty_9c1e…",
"starts_at": "2026-11-03T10:00:00+11:00",
"client": { "first_name": "Mei", "last_name": "Chen", "email": "mei@example.com",
"mobile": "0400 000 000", "date_of_birth": "1988-04-12" },
"notes": "Typed by the client",
"consents": { "terms": { "accepted": true, "version": "3" },
"privacy_policy": { "accepted": true, "version": "2" },
"marketing_email": false, "marketing_sms": false,
"ip_address": "203.0.113.9" },
"external_reference": "web-order-8812"
}
The answer is 201 with the appointment. The client is matched to an existing record where the name and one of email, date of birth or mobile agree, otherwise a new record is made; a matched record is never changed by what you send. The clinic's own confirmation email and SMS then go to the client, as they would from the booking page.
- Where the location requires terms or a privacy policy, show the client the location's
textand sendaccepted: truewith theversionyou showed; otherwise the booking is422 terms_required, or422 terms_outdatedif the text has changed since (details.current_versionsays which). A privacy policy ends with a section Daobook keeps current, naming the services that handle the patient's information, so its version also moves when that section does, such as when the clinic connects a new service: read the location when you show the form, not once at start-up. - A type with
payment_required_at_bookingtakes a deposit or a card hold, which the API does not handle: the answer is422 payment_requiredwith the location'sbooking_page_urlindetails. Send the client there. - A time nobody offers any more is
409 slot_unavailable: read availability again and let the client choose.- After booking.
GET /v1/appointments/{id}returns the booking withself_service: whether the client may move it (can_reschedule), what a cancel would do now (cancel_outcomeiscancelorlate_cancel, which succeed with that status, oroffer_rescheduleorcall, which the API refuses) and the clinic'snotice_hours.GET /v1/appointmentslists this business's API-made bookings, most recently updated first, withupdated_since,location_id,practitioner_id,status,kindandexternal_referencefilters, or a diary window withfromandto(the Diary section). Nothing stored about the client comes back, onlyclient_id. Every appointment carrieskind: a booking made through the API is alwaysbooking. - Move, cancel or confirm.
POST /v1/appointments/{id}/reschedulewith{ "starts_at": "…" }keeps the practitioner, location and type;POST /v1/appointments/{id}/canceltakes an optional{ "reason": "…" }. Both follow the clinic's rules for a client's own changes, the same rules as the manage-your-appointment link in the confirmation email, and may be refused with422 reschedule_not_allowedor422 cancellation_not_allowed. Either refusal carriesdetails.outcome, what a cancel would do now (cancel,late_cancel,offer_rescheduleorcall): the clinic's minimum notice refuses a move as it refuses a cancel, so inside it a client may be able to cancel but not move, or only phone the clinic.POST /v1/appointments/{id}/confirmis the client's "yes, I'll be there", the same as a YES reply to the clinic's reminder; confirming again is200unchanged, and a booking that is not scheduled or has started is422 confirmation_not_allowed.
- After booking.
A key with appointments:book reaches only the bookings this business made through the API, unless a caller has been verified (next section) or the key also reads the diary (the Diary section). The clinic's front desk sees every API booking in its calendar, marked as booked online, and may move or cancel it like any other; your software hears about that through events. An app acting as a person (Acting as a person) reaches more: beside the API's own bookings, every upcoming client booking at the locations where the person's role manages the calendar, whoever made it, and moves, cancels or confirms it as the person would in the calendar, the staff rules applying rather than the client's: the clinic's notice window does not refuse it, the person is recorded as who moved or cancelled, and the client is told as their own preference says. A cancel may say whose cancellation it is with cancelled_by (client or clinic); a refusal carries details.reason, the booking's own state (cancelled, completed, paid, status).
Waiting list
When nothing on offer suits, put the caller on the location's waiting list with what they are waiting for. Daobook does the rest: when a time frees that suits them, the clinic texts and emails them a link to book it (by hand, or automatically where the clinic has turned that on), it goes to whoever books first, and a booking takes them off the list. Your software never sees who else is waiting. Needs waiting_list:write.
POST /v1/waiting_list_entries
Authorization: Bearer dbk_live_…
Idempotency-Key: 2c7a1f0e-3b4d-4e5f-8a9b-0c1d2e3f4a5b
Content-Type: application/json
{
"location_id": "loc_41aa…",
"practitioner_id": "prac_77d0…",
"appointment_type_id": "apty_9c1e…",
"preferred_days": ["tuesday", "thursday"],
"preferred_times": ["morning"],
"remove_on": "2026-12-19",
"client": { "first_name": "Mei", "last_name": "Chen", "mobile": "0400 000 000", "email": "mei@example.com" },
"notes": "Wants the earliest Tuesday; took the call 19 Oct.",
"external_reference": "call-8812"
}
- Everything but
location_idandclientis optional: no practitioner means anyone at the location, no type means any, no days or times means any, noremove_onmeans the clinic's default. A practitioner or type named must be offered online at the location, as for a booking (422 not_bookable). - The client needs a first name, a last name and a mobile or an email, so the offer can reach them (
422 client_details_requiredotherwise). The details are matched to an existing client record where the name and one of email, date of birth or mobile agree; otherwise a new record is made, and a matched record is never changed. - The answer is
201with the entry: ids,status(waiting,offeredonce a time has been sent to them,booked, orexpired), the wish in words,remove_on,offered_at. Nothing stored about the client comes back. A client who already has an open entry made through the API at that location gets that entry back with200rather than a second. GET /v1/waiting_list_entrieslists the entries your software made, most recently updated first, withstatus=,location_id=,external_reference=andupdated_since=;GET /v1/waiting_list_entries/{id}fetches one.POST /v1/waiting_list_entries/{id}/removetakes the caller off the list; a booked entry is closed (422 entry_closed).- A caller who phones back after the clinic's text is booked with the ordinary
POST /v1/appointmentsat that time, and comes off the list by themselves.
Caller check
For a phone receptionist. A caller's other bookings, the ones the front desk or the booking page made, are reached through a check on who is calling, never through a patient list or a search. Needs callers:verify, and appointments:book for the appointment endpoints. Every check is written to the patient's record, where the clinic sees it as a line by your key.
- The greeting. At the start of the call,
POST /v1/caller_lookupswith the number the caller rang from.
{ "phone": "+61412345678" }
The answer is { "object": "caller_lookup", "known": true, "first_name": "Mei" } when the number is exactly one live patient's at the key's locations, so you can say hello by name; known with first_name: null when several patients share the number (ask who is speaking); known: false otherwise. Nothing is stored.
- The check. Before anything about their bookings is said or done, ask the caller's date of birth and
POST /v1/caller_verifications:
{ "phone": "+61412345678", "date_of_birth": "1984-03-09" }
or, when the number was unknown or shared, or the caller is on someone else's phone:
{ "first_name": "Mei", "last_name": "Chen", "date_of_birth": "1984-03-09" }
Send all three and the number is tried first, the names second. Exactly one patient must match: the date exact, the name within the tolerance a booking's match allows (a transcription slip, a nickname, the preferred name the clinic keeps), the number however it was written. The answer is 201 with the verification: its id (vrf_…), method (phone or name), the caller's first_name and expires_at, fifteen minutes on. Anything else, a wrong date, a wrong name, two matches or none, is 404 verification_failed, with nothing to say which. After five failures for one number or name in an hour, or fifty for the key, checks are 429 too_many_attempts until the hour is out: offer a callback.
Their bookings.
GET /v1/caller_verifications/{id}/appointmentslists the verified caller's upcoming bookings at the key's locations, however they were made, soonest first, each withself_service. Cancelled, completed and past bookings are not there.Acting on one. Send the verification's id as a header on the appointment endpoints:
POST /v1/appointments/appt_3f2b…/reschedule
Authorization: Bearer dbk_live_…
Caller-Verification: vrf_9c1e…
Idempotency-Key: 1b2c3d4e-…
Content-Type: application/json
With it, GET /v1/appointments/{id}, …/reschedule, …/cancel and …/confirm reach the verified caller's own bookings as well as the ones your software made, under the same patient rules as before; without it, or with an expired or another key's verification, a desk-made booking stays a 404. The verification is only ever that one caller's: another patient's booking is a 404 with it too.
What a verified caller never gets: anyone else's bookings, their own past visits, notes, invoices, or the stored details of their record. The receptionist already has the caller's name and number from the call.
Diary
For a phone receptionist that tells a caller what is on today, a reminder or reporting service, or a practitioner's assistant. Needs appointments:read, ticked as Read the diary when the key is made: the first scope that acts for the practice rather than for a patient. With it, GET /v1/appointments and GET /v1/appointments/{id} reach every appointment at the key's locations, whoever made it: the front desk's, the booking page's and the API's own.
- A diary window.
GET /v1/appointments?from=2026-11-03&to=2026-11-03lists the day soonest first,fromandtobeing dates in each location's own zone and at most 31 days apart; page with the cursor it hands back. Addlocation_id,practitioner_id(a member, present or past, at the key's locations),client_id(one client's bookings),statusorkind. A window is given instead ofupdated_since, not with it. - By last change. Without a window the list is most recently updated first, as for the API's own bookings, with
updated_sincefor syncing. - Kinds. Every appointment carries
kind:booking, one client's appointment;class_session, a class with places and no client of its own (client_idis null);class_place, one client's place in a session, pointing at it withclass_session_id. A booking made through the API is alwaysbooking. - What comes back is what came back before: ids, status, times in the location's zone,
source(staff,onlineorapi), stamps andself_service. No name, no contact detail, no note, under this scope or any key. - Nothing may be changed under it. Booking, moving, cancelling and confirming keep
appointments:bookand reach only the bookings your software made (and a verified caller's own). A read-only key sending a write gets403 insufficient_scopenamingappointments:book. - Every read is on the record. Each appointment read under the scope is written on that client's record as a line by your key, one per hour, which the clinic sees on the client's Who Opened This Record and in its Activity Log. Appointment details are health information; the clinic allows this with one tick on the key, and takes it back by revoking the key.
Clients
For a name on the diary, a CRM or mailing tool syncing the client list, a practitioner's own assistant, or a website account that keeps its own id for a patient. Two scopes, ticked as Read client names and contact details (clients:read) and Add and update clients (clients:write, which carries the read) when the key is made. The reach is the front desk's at the key's locations: a shared location on the key reaches every shared location's clients, and a location that keeps a private client list is reached only when it is on the key.
- The client.
GET /v1/clients/{id}answers identity (first_name,last_name,preferred_name,title,pronouns,sex,date_of_birth), contact (email,mobile,home_phone,work_phone,address),occupation, the homelocation_idand the client'spractitioner_id,status(activeorarchived), and three groups of flags:contact(what the patient or the channel says,ok,declined,opted_outorundeliverable: send on a channel only when it isok),remindersandmarketing(the switches as the senders apply them, so a declined channel readsfalsewhatever the switch), plusexternal_references(your own ids for them,systemandreference) and the first and last visit. Never a note, an alert, allergies, medications, a file, a fund or insurance number, or a balance. - The list.
GET /v1/clientsis most recently changed first withupdated_sincefor syncing, filtered bylocation_idandstatus(activeby default,archivedorall; a deleted client is never listed). Or look one up:email=(exact),mobile=(by digits, however typed),name=(as the client list searches) orexternal_reference=system:reference, at most 20 matches and no cursor; several lookups must all agree. - Names on the diary.
GET /v1/appointments?expand=clientandGET /v1/appointments/{id}?expand=clientput the client on each appointment, underclients:readas well as the appointment scope. Withoutclients:read,expand=clientis403 insufficient_scope. - Adding.
POST /v1/clientswith anIdempotency-Key:first_nameandlast_name, the fields above as you have them,location_idwhen the key reaches more than one location,practitioner_idfor a member there, andreminders,marketingandexternal_referencesif you wish. The client gets the booking page's defaults and the business owner as their owner. The details are matched to the existing clients by the booking floor's rule (the name, and one of email, date of birth or mobile): a match is409 duplicate_clientcarrying the match indetails.clientand what agreed indetails.matched_on, or, withon_duplicate: "return", the match answered200unchanged, which is find-or-create. The same email or mobile under another name does not refuse: the client is added and the front desk is left a note to review. - Changing.
PATCH /v1/clients/{id}with the fields to change;nullclears an optional one;address,remindersandmarketingchange by the keys given; an external reference given replaces the one stored for its system.contactand the home location are not taken. An archived client is422 client_archived: the clinic restores them first. There is no archive, delete or merge through the API; the events below tell you when the clinic does them. - Consent. Setting a
marketingchannel on records the consent as asserted through the API, dated, in the same record a STOP reply or an unsubscribe writes; the business holds that consent (the Spam Act in Australia). Acontactstate ofdeclinedis the patient's own request not to be contacted that way, recorded by the desk: honour it in anything you send. - Every read is on the record. Each client listed, shown, expanded or carried in a
409, and each one your write answers, is written on that client's record as a line by your key, one per hour, which the clinic sees on the client's Who Opened This Record and in its Activity Log.
Events and webhooks
Every change to an API-made booking or waiting list entry is recorded as an event; once the business holds a live key with appointments:read, every appointment's change is too, and once one reads clients (clients:read), every client's. A key hears about the API's own bookings with appointments:book, about every appointment with appointments:read, about the waiting list with waiting_list:write, and about clients with clients:read:
| Type | When |
|---|---|
appointment.created | Your software booked it; with appointments:read, anyone did |
appointment.rescheduled | Its time changed, by your software or at the front desk |
appointment.cancelled | It was cancelled, by anyone |
appointment.updated | Something else about it changed: its status (arrived, in progress, completed, no show), its end time, its confirmation, its type or its location |
appointment.deleted | The clinic deleted it |
waiting_list_entry.created | Your software put a caller on the waiting list |
waiting_list_entry.offered | The clinic sent them a freed time; data.offer says which |
waiting_list_entry.booked | They booked, and came off the list |
waiting_list_entry.removed | They were taken off the list, by your software or the clinic |
client.created | A client was added, by your software or at the front desk |
client.updated | Their name, contact details, address, practitioner, home location or contact flags changed |
client.archived, client.restored | The clinic archived them, or brought them back |
client.deleted | The clinic deleted them |
client.merged | The clinic merged them into another client; data.merged_into says which |
test.sent | The account owner pressed Send test in Settings |
Events are thin: data.object holds the booking's ids, status and times (for a client event, the client's id, status, location and practitioner), never a name, a contact detail or a note, and data.previous holds the old values of whatever changed. Fetch the record with your key for anything more.
{ "id": "evt_5d0c…", "object": "event", "type": "appointment.rescheduled",
"timestamp": "2026-11-01T02:14:07Z",
"data": {
"object": { "id": "appt_3f2b…", "object": "appointment", "status": "scheduled",
"starts_at": "2026-11-04T09:00:00+11:00", "ends_at": "2026-11-04T10:00:00+11:00",
"location_id": "loc_41aa…", "practitioner_id": "prac_77d0…",
"appointment_type_id": "apty_9c1e…", "external_reference": "web-order-8812" },
"previous": { "starts_at": "2026-11-03T10:00:00+11:00", "ends_at": "2026-11-03T11:00:00+11:00" } } }
The feed. GET /v1/events lists events oldest first, filtered with types=appointment.cancelled,appointment.rescheduled and paged with the cursor, so your software keeps its place. GET /v1/events/{id} fetches one. Events are kept for 30 days. The feed is the recovery path after missed webhooks, and the whole integration for software that cannot receive them.
Webhooks. The account owner adds an endpoint in Settings → Integrations → Developer API: an https URL on a public address, all events or only some, and, once a key reads the diary, which bookings: only those made through the API (the default, so an endpoint for a website is not sent the whole diary the day a receptionist's key is made) or every booking in the diary; and, once a key reads clients, whether Client changes come too (off unless ticked). The endpoint's secret (whsec_…) is shown once. Daobook then POSTs each event to it:
- Delivery follows Standard Webhooks 1.0.0. Verify with the official library for your language and the endpoint's secret rather than with code of your own. The headers are
webhook-id(the event's id, the same on every retry, so a duplicate can be dropped),webhook-timestamp(the attempt's Unix seconds; reject one more than five minutes out) andwebhook-signature(v1,and base64 of HMAC-SHA256 over{webhook-id}.{webhook-timestamp}.{body}). - Answer any
2xxwithin 15 seconds and do the work afterwards. A redirect is a failure and is not followed. A410disables the endpoint at once. - Retries: at once, then 5 seconds, 5 minutes, 30 minutes, 2, 5, 10, 14, 20 and 24 hours. After three days without a success the endpoint is disabled and the owner is emailed; to resume, the owner removes it and adds it again.
- Order is not guaranteed. The event's
timestampand a freshGETdecide what is current. - Rotating the secret in Settings keeps the old one signing for 24 hours, during which each delivery carries both signatures, space-separated.
- Send test in Settings delivers a
test.sentevent to one endpoint, so a receiver can be checked before anything real happens.
In Ruby, with the official standardwebhooks gem:
require 'standardwebhooks'
webhook = StandardWebhooks::Webhook.new(ENV.fetch('DAOBOOK_WEBHOOK_SECRET'))
event = webhook.verify(request.raw_post, request.headers) # raises StandardWebhooks::WebhookVerificationError
Errors
{ "error": { "type": "conflict", "code": "slot_unavailable",
"message": "That time is no longer available.",
"param": "starts_at", "details": {},
"request_id": "0b9f6c1e-…",
"doc_url": "https://www.daobook.com.au/developers/errors#slot_unavailable" } }
code is the contract: stable, documented, and what your software branches on to choose its own words for the client. message is for you and may be reworded. A request with several bad fields also carries error.errors, a list of { param, code, message }.
| Status | type | Means |
|---|---|---|
| 400 | invalid_request | The request is malformed: a bug in the integration |
| 401 | authentication | No key, or not a live one |
| 403 | permission | A live key that may not do this |
| 404 | not_found | No such thing, or not this key's to see |
| 409 | conflict | Something changed; choose again |
| 422 | refused | Well-formed, but a rule of the clinic says no |
| 429 | rate_limited | Too many requests |
| 500, 503 | server | Our fault. Retry with the same Idempotency-Key |
Every code, with what to do about it, is in the error catalogue.
Testing
Build against a trial Daobook account of your own rather than the clinic's live one: sign up at daobook.com.au, set up a location, a practitioner and an appointment type offered for online booking, accept the API terms under Settings → Integrations → Developer API and make a key there; ask us to keep the account past the trial. Bookings made there send real emails and SMS to the addresses you give, so use your own. There is no separate sandbox or test key yet.
Changes and versions
- The major version is in the path:
/v1. Within it we only add: a new endpoint, parameter, field, event type, enumerated value or error code may appear at any time, each noted on the changelog. Your software should ignore what it does not recognise, treat ids as opaque, and never parse a message. - Nothing in version 1 is renamed, removed or changes meaning without at least twelve months' notice. A retired endpoint keeps working through that notice, answers with
DeprecationandSunsetheaders and aLinkto the notice, and every business whose keys have called it is emailed. - Version 1 is young: how it works may still change with notice to the businesses using it, within the change policy above.
Support
Write to admin@daobook.com.au with the X-Request-Id of the response you are asking about. Keys, scopes, webhook endpoints and recent deliveries are all visible to the account owner in Settings → Integrations → Developer API. The API terms of use say what we each undertake.