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

  1. 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.
  2. 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.
  3. 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_…"] }
  1. Read what can be booked: GET /v1/locations, then GET /v1/appointment_types?location_id=… and GET /v1/practitioners?location_id=….
  2. Read free times: GET /v1/availability?location_id=…&appointment_type_id=…&from=2026-11-02&to=2026-11-08.
  3. Book: POST /v1/appointments with an Idempotency-Key header. 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:write and callers:verify (the sections below), appointments:read (read every appointment in the diary, whoever made it; the Diary section), and clients:read and clients:write (who the clients are, and adding or changing them; the Clients section). GET /v1/account is 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 is 403 api_not_enabled; one whose subscription is not active is 403 account_inactive; one without the scope an endpoint needs is 403 insufficient_scope, naming the scope in details.required_scope.
  • Send a User-Agent that names your integration and a contact address, such as ClinicSite/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. Every 401 the API answers carries WWW-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 with client_name and redirect_uris (https, or http://localhost and http://127.0.0.1 with any port), token_endpoint_auth_method absent or "none"; the answer is a client_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 as client_id is 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/authorize with response_type=code, client_id, redirect_uri, scope, state, code_challenge and code_challenge_method=S256, and, if you will also use the MCP server, resource (https://api.daobook.com.au or https://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 to redirect_uri?code=…&state=…; Cancel sends error=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 as error and error_description with your state.
  • Scopes a person may grant: availability:read (any member); appointments:read, clients:read and clients:write (owner, admin, practitioner, receptionist); appointments:book and waiting_list:write (owner, admin, receptionist, who manage the calendar); offline_access for a refresh token. callers:verify is never a person's. A scope the person cannot hold anywhere is left out and scope in 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 as client_secret). The answer is access_token (dbk_access_…, one hour), token_type: "Bearer", expires_in: 3600, refresh_token (dbk_refresh_…) and scope. grant_type=refresh_token with refresh_token and client_id answers a new pair and retires the old refresh token; a retired refresh token presented again revokes every token of that connection and answers invalid_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/revoke with token (RFC 7009), always 200; revoking the connection's last refresh token ends it.
  • Using the token. Authorization: Bearer dbk_access_… on any /v1 call. GET /v1/account answers practitioner_id, app_id and app_name beside the business (key_id is null). 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 is 401 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. Under appointments:book the 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 /v1 a token has a key's limits.
  • The MCP server. https://api.daobook.com.au/mcp is 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/mcp and the 401 above, 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 carries iss (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)

  1. 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.
  2. Enter the address above. If Claude asks how it should identify itself (OAuth client), choose Use Claude's published identity. Click Add.
  3. Click Connect. Daobook opens: sign in, and click Allow on the screen that says Claude wants to connect to your Daobook.
  4. 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.

ToolWhat it doesScope
get_accountThe business, the person, the app, the scopes held and the locations reachednone
list_locations, list_practitioners, list_appointment_typesWhat can be booked onlineavailability:read
find_free_timesFree times for an appointment type, a week at a timeavailability:read
list_appointments, get_appointmentThe diary; under appointments:book, also the bookings the person may changeappointments:read or appointments:book
book_appointmentBook under the online-booking rulesappointments:book
reschedule_appointment, cancel_appointment, confirm_appointmentMove, cancel or confirm a booking (changes)appointments:book
add_to_waiting_list, list_waiting_list_entries, get_waiting_list_entry, remove_from_waiting_listPut someone on the waiting list, and read or remove the entries it added (removing one changes)waiting_list:write
find_client, get_clientFind a client by name, email, mobile or reference, and read their contact detailsclients:read
create_client, update_clientAdd 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/json and 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 in time_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's timestamp) are UTC and end in Z. Dates are YYYY-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 with limit (default 50, at most 100) and an opaque cursor. To continue, send cursor with the same filters. There are no totals.
  • Named actions. A change that carries a business rule is a POST to 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 on POST /appointments and accepted on every POST. The same key with the same body replays the first answer, marked Idempotent-Replayed: true; with a different body it is 422 idempotency_key_reused; while the first is still running, 409 request_in_progress. A 5xx or a 429 is 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_limited with Retry-After in seconds.
  • Request ids. Every response carries X-Request-Id, and every error body repeats it as request_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).

  1. Locations. GET /v1/locations lists the key's locations offered for online booking. Each carries its time_zone, its booking_page_url, the required_client_fields the clinic's booking form insists on, and terms and privacy_policy, each with whether it is required, the current version and the text to show the client.
  2. Appointment types and practitioners. GET /v1/appointment_types?location_id=… (optionally &practitioner_id=…) and GET /v1/practitioners?location_id=…. A type carries duration_minutes, price, practitioner_ids and payment_required_at_booking; a practitioner carries online_booking.minimum_notice_hours and online_booking.window_days.
  3. 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. Add practitioner_id for 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.
  4. 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 text and send accepted: true with the version you showed; otherwise the booking is 422 terms_required, or 422 terms_outdated if the text has changed since (details.current_version says 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_booking takes a deposit or a card hold, which the API does not handle: the answer is 422 payment_required with the location's booking_page_url in details. Send the client there.
  • A time nobody offers any more is 409 slot_unavailable: read availability again and let the client choose.
    1. After booking. GET /v1/appointments/{id} returns the booking with self_service: whether the client may move it (can_reschedule), what a cancel would do now (cancel_outcome is cancel or late_cancel, which succeed with that status, or offer_reschedule or call, which the API refuses) and the clinic's notice_hours. GET /v1/appointments lists this business's API-made bookings, most recently updated first, with updated_since, location_id, practitioner_id, status, kind and external_reference filters, or a diary window with from and to (the Diary section). Nothing stored about the client comes back, only client_id. Every appointment carries kind: a booking made through the API is always booking.
    2. Move, cancel or confirm. POST /v1/appointments/{id}/reschedule with { "starts_at": "…" } keeps the practitioner, location and type; POST /v1/appointments/{id}/cancel takes 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 with 422 reschedule_not_allowed or 422 cancellation_not_allowed. Either refusal carries details.outcome, what a cancel would do now (cancel, late_cancel, offer_reschedule or call): 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}/confirm is the client's "yes, I'll be there", the same as a YES reply to the clinic's reminder; confirming again is 200 unchanged, and a booking that is not scheduled or has started is 422 confirmation_not_allowed.

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_id and client is optional: no practitioner means anyone at the location, no type means any, no days or times means any, no remove_on means 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_required otherwise). 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 201 with the entry: ids, status (waiting, offered once a time has been sent to them, booked, or expired), 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 with 200 rather than a second.
  • GET /v1/waiting_list_entries lists the entries your software made, most recently updated first, with status=, location_id=, external_reference= and updated_since=; GET /v1/waiting_list_entries/{id} fetches one. POST /v1/waiting_list_entries/{id}/remove takes 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/appointments at 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.

  1. The greeting. At the start of the call, POST /v1/caller_lookups with 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.

  1. 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.

  1. Their bookings. GET /v1/caller_verifications/{id}/appointments lists the verified caller's upcoming bookings at the key's locations, however they were made, soonest first, each with self_service. Cancelled, completed and past bookings are not there.

  2. 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-03 lists the day soonest first, from and to being dates in each location's own zone and at most 31 days apart; page with the cursor it hands back. Add location_id, practitioner_id (a member, present or past, at the key's locations), client_id (one client's bookings), status or kind. A window is given instead of updated_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_since for syncing.
  • Kinds. Every appointment carries kind: booking, one client's appointment; class_session, a class with places and no client of its own (client_id is null); class_place, one client's place in a session, pointing at it with class_session_id. A booking made through the API is always booking.
  • What comes back is what came back before: ids, status, times in the location's zone, source (staff, online or api), stamps and self_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:book and reach only the bookings your software made (and a verified caller's own). A read-only key sending a write gets 403 insufficient_scope naming appointments: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 home location_id and the client's practitioner_id, status (active or archived), and three groups of flags: contact (what the patient or the channel says, ok, declined, opted_out or undeliverable: send on a channel only when it is ok), reminders and marketing (the switches as the senders apply them, so a declined channel reads false whatever the switch), plus external_references (your own ids for them, system and reference) 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/clients is most recently changed first with updated_since for syncing, filtered by location_id and status (active by default, archived or all; a deleted client is never listed). Or look one up: email= (exact), mobile= (by digits, however typed), name= (as the client list searches) or external_reference=system:reference, at most 20 matches and no cursor; several lookups must all agree.
  • Names on the diary. GET /v1/appointments?expand=client and GET /v1/appointments/{id}?expand=client put the client on each appointment, under clients:read as well as the appointment scope. Without clients:read, expand=client is 403 insufficient_scope.
  • Adding. POST /v1/clients with an Idempotency-Key: first_name and last_name, the fields above as you have them, location_id when the key reaches more than one location, practitioner_id for a member there, and reminders, marketing and external_references if 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 is 409 duplicate_client carrying the match in details.client and what agreed in details.matched_on, or, with on_duplicate: "return", the match answered 200 unchanged, 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; null clears an optional one; address, reminders and marketing change by the keys given; an external reference given replaces the one stored for its system. contact and the home location are not taken. An archived client is 422 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 marketing channel 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). A contact state of declined is 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:

TypeWhen
appointment.createdYour software booked it; with appointments:read, anyone did
appointment.rescheduledIts time changed, by your software or at the front desk
appointment.cancelledIt was cancelled, by anyone
appointment.updatedSomething else about it changed: its status (arrived, in progress, completed, no show), its end time, its confirmation, its type or its location
appointment.deletedThe clinic deleted it
waiting_list_entry.createdYour software put a caller on the waiting list
waiting_list_entry.offeredThe clinic sent them a freed time; data.offer says which
waiting_list_entry.bookedThey booked, and came off the list
waiting_list_entry.removedThey were taken off the list, by your software or the clinic
client.createdA client was added, by your software or at the front desk
client.updatedTheir name, contact details, address, practitioner, home location or contact flags changed
client.archived, client.restoredThe clinic archived them, or brought them back
client.deletedThe clinic deleted them
client.mergedThe clinic merged them into another client; data.merged_into says which
test.sentThe 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) and webhook-signature (v1, and base64 of HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{body}).
  • Answer any 2xx within 15 seconds and do the work afterwards. A redirect is a failure and is not followed. A 410 disables 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 timestamp and a fresh GET decide 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.sent event 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 }.

StatustypeMeans
400invalid_requestThe request is malformed: a bug in the integration
401authenticationNo key, or not a live one
403permissionA live key that may not do this
404not_foundNo such thing, or not this key's to see
409conflictSomething changed; choose again
422refusedWell-formed, but a rule of the clinic says no
429rate_limitedToo many requests
500, 503serverOur 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 Deprecation and Sunset headers and a Link to 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.