Changelog
Newest first. Each entry is dated by the change that made it and has its own place on this page to link to. Changes before 2026-10-30 shipped before API versions existed, so no version was named for them; the ones that could break a client written against what came before are marked Breaking. How versions work: Versioning.
Trips that span days: startDate, endDate and days on POST /appointments and the appointment reads
2026-11-02 · After release 17
- A service can last whole days (1 to 60) instead of minutes: a trip.
POST /appointments(bookings:write) books one by its days,startDate(YYYY-MM-DD, the business's calendar) withendDate(inclusive) or withdays, instead ofstartAtorlocalTime. It covers those days in full on the business's clock, whatever the clocks do in between, is priced once per booking, holds its person (or nobody, withoutstaffId) and the resources its service needs for every one of them, takes no extras and does not repeat. The answer hasstartDate,endDateanddays. - Refused with
422naming the field, nothing booked: a time for a service of days, a date that is not realYYYY-MM-DD, noendDateordays, anendDatebeforestartDate,endDateanddaystogether, more than 60 days, arepeat, or any of the three for a service of minutes.409when the trip's person is already booked on those days, or no resource of a kind it needs is free on them. GET /appointmentsandGET /appointments/{id}carrystartDate,endDateanddayson a trip, afternotifyClient; every other booking reads exactly as before.- Only the business books a trip: the keyless doors (
GET /services,GET /availability,POST /book,POST /visits/book) never list or book a service of days. Additive: the same API version, and nothing changes for a service of minutes.
A sandbox's test payments: GET /sandbox/payments and the sandbox_payments MCP tool
2026-11-02 · After release 17
GET /api/v1/{slug}/sandbox/paymentslists every payment a sandbox made in test mode, newest first, as its admin's new Test payments page (beside the Outbox) does: checkouts, the payments they made, card holds, no-show charges and refunds (kind), each with itsoutcome(paid,declined,abandoned,open,card_saved,refundedorpartly_refunded),amountCentsandcurrency,refundedCents,description, the booking it belongs to (appointmentId) and the booking or sale (belongsTo), its checkout and payment ids, and when it was made. Nothing in it names a client.- Filter with
kindandsince, and page withlimit(1 to 100) andcursor, as onGET /sandbox/outbox; a filter that is not valid is422withfieldErrors. It takes a sandbox's test key withreports:read, the money reports' scope, and allows 30 calls a minute per sandbox and per key. A live business answers404withcode: "NOT_A_SANDBOX", and a key of the other mode403withcode: "LIVEMODE_MISMATCH". - At a sandbox, the business MCP server's
tools/listaddssandbox_paymentsaftersandbox_outbox, for a credential withreports:read: the same payments, filters, cursor and limits, read-only and idempotent, with anoutputSchema. A live business never lists it, and itstools/listis unchanged. - Additive: the same API version, and nothing changes at a live business.
Quiet bookings: notifyClient on POST /appointments and the appointment reads
2026-11-02 · After release 17
POST /appointments(bookings:write) takesnotifyClient.falsemakes a booking whose client Bookatu never messages, by email or text: no confirmation, booking request or decline, reminder, ask to confirm, thank-you or review request, move or cancellation notice, deposit or payment request, "did you make this booking?" check or Quick swap message. The team's own alerts and the webhooks are unchanged, and marketing still follows the client's own consent. A series (repeat) made with it is quiet on every visit.true, or left out, is the usual booking.- Anything but
trueorfalseis422withfieldErrors.notifyClient, and so isnotifyClient: falsewithrequestDeposit: true, since the pay link is sent to the client. Nothing is booked. GET /appointmentsandGET /appointments/{id}carrynotifyClienton every booking, aftercreatedAt.POST /appointments/{id}/deposit-requestanswers409withreason: "client_messages_off"for a booking whose client is not messaged, and makes no link.- The keyless doors (
POST /book,POST /visits/book) do not takenotifyClient: a booking made there always tells the person who booked. Additive: the same API version, and nothing changes for a booking made without it.
Walk-in wait time: GET /walk-in, PUT and DELETE /walk-in/manual, and three MCP tools
2026-11-02 · Release 17
- New keyless
GET /walk-in: the live walk-in wait of a business (status,mode,state,waitMin,nextAt,updatedAt,timeZone,manual,closed,busycounts and each walk-in service with itsbookUrl), the same answer as its booking page, its walk-in widget and MCP give for the same moment, kept about a minute. A business with it off answers{ "status": "off" }with no numbers, and aserviceIdthat is not one of its walk-in services is 404SERVICE_NOT_FOUND. Counts only: no client or team member is ever named. - New
PUT /walk-in/manual(minutes0 to 240 ornotTaking: true, andexpiry1h,2horend_of_day) andDELETE /walk-in/manual, for a key withbookings:write: the wait set by hand until it lapses, and back to the automatic wait (cleared).PUTanswers 409WALK_IN_OFFwhile walk-in wait times are off. Each set and clear is in the audit log, naming the key. - Three MCP tools after
get_business:get_walk_in_wait(availability:read, the same rule asget_availability, withmessagebesidestatus: "off"), andset_walk_in_waitandclear_walk_in_wait(bookings:write). - New error codes
SERVICE_NOT_FOUNDandWALK_IN_OFF. Additive: the same API version, and nothing changes for a business that never turns walk-in wait times on.
MCP create_booking and create_visit take the person's answers to the business's booking questions
2026-11-02 · Release 17
create_bookingandcreate_visittake an optionalanswersobject,{ [questionId]: answer }: the person's answers to the business's booking questions, with the same shape, validation and limits asanswersonPOST /book(for a visit, each question once for the whole visit). Until now neither tool took answers, so a service with a required question could not be booked over MCP at all.list_serviceslists each service's booking questions asquestions, in the order they are asked, exactly asGET /servicesdoes. A business that asks none lists none, so its answer is unchanged.- A required question left out is refused with
ANSWERS_REQUIREDandmissingQuestionIds, an answer that cannot be read withANSWERS_INVALIDandinvalidAnswers, and nothing is booked, withbookingPageUrlbeside them as before. The message now tells the agent to ask the person the questions and send their answers, or to send the person tobookingPageUrl. - The answers are kept for the business only: never echoed back in a tool result and never in a webhook payload. Additive: the same API version.
MCP create_booking and create_visit refuse a service that takes a deposit, with the booking page to send the customer to
2026-10-31 · Release 15 hotfix · Breaking
- Over MCP,
create_bookingandcreate_visitno longer book a service (or a visit) that takes a deposit. They refuse it withcode: "DEPOSIT_NEEDS_BOOKING_PAGE"andbookingPageUrl, the business's booking page (opened on the service forcreate_booking), where the customer books it and pays the deposit, asPOST /bookandPOST /visits/bookdo. Nothing is written: no booking, no held slot, no client record and no message. - The refusal is a tool error (
isError: true,structuredContent.errorwithcode,messageandbookingPageUrl; nolivemodethere, as on every refusal: it is inresult._meta) for a client that sends a 2025MCP-Protocol-Versionheader, and the JSON-RPC error withcodeandbookingPageUrlinerror.dataotherwise. A sandbox answers the same, with its test key. - There is no
payLinkin either tool's answer any more. Before this change such a booking was madependingand answered with apayLinkto the booking's success page, which takes no payment, so the customer could not pay and the slot was held until it was released. A service that takes no deposit books exactly as before, and the team's RESTPOST /appointments(withrequestDeposit) is unchanged. - At a business whose booking page is not published yet,
bookingPageUrlshows the customer a coming soon page until the owner publishes it, so the refusal there also carriesbookingPagePublished: false, and its message says to publish the booking page first. - Marked breaking, as the same change to
POST /bookwas (2026-10-29), yet given to every version at once rather than held for a new one: thepayLinkopened a page that takes no payment, so there is no working behaviour for an older version to keep.
The business MCP server at a sandbox: three test tools, livemode in structuredContent, and LIVEMODE_MISMATCH
2026-10-30 · Release 15
- At a sandbox,
tools/listadds three tools after the rest, each only for a credential that may call it:sandbox_outbox(customers:read: the test outbox, asGET /sandbox/outboxanswers it),sandbox_simulate(bookings:write: the six events ofPOST /sandbox/simulate/{event}, for the sandbox's own bookings only) andsandbox_reset(the sandbox's ownbk_test_key withbookings:write, never a connected app). Each shares its REST twin's limits. A live business never lists them, and itstools/listis unchanged. - Every successful tool result says
livemodeinstructuredContent(falseat a sandbox,trueat a live business), and a write now answers withstructuredContenttoo: its text's JSON pluslivemode. The text block is unchanged. A refusal (isError) has nolivemodeinstructuredContent;result._meta["com.bookatu/livemode"]says it on every tool result, and theBookatu-Livemodeheader on every answer. - Over MCP, a credential of the other mode than its business, or a call whose
Bookatu-Livemodeheader names the other kind, is refused before anything runs:403, the JSON-RPC error-32000, witherror.data.code: "LIVEMODE_MISMATCH",keyLivemodeandbusinessLivemode. - A guide to testing an AI agent against a sandbox, from Claude Desktop, Cursor or the MCP Inspector:
/developers/testing-ai-agents.
Version 2026-10-30: API versions, rate-limit headers and test mode
2026-10-30 · Release 15 · version 2026-10-30
- The first named API version,
2026-10-30. SendBookatu-Version: 2026-10-30to ask for it; every answer says the version it was answered in, inBookatu-Version. Every key and connected app made before this release is pinned to it, and a new one is pinned to the version current when it is made. - A
Bookatu-Versionthat is not a version the API answers in is refused before anything runs:400withcode: "UNSUPPORTED_API_VERSION"andsupportedVersions. - Every answer of the v1 API and the MCP servers carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetfor the window closest to its limit, andRateLimit-Policynaming every window the call was counted against; every429carriesRetry-After. A browser may read all of them (Access-Control-Expose-Headers). - Each key is now also counted on its own, at its plan's numbers, beside its business's keys together: a key on the standard tier, every key unless Bookatu changes it, sees no change, and a call refused before the endpoint counts it (a missing scope, another business's key) is not counted on the key, as before. A Bookatu platform admin can put a live key on the raised tier.
- Test mode: sandbox businesses with sample data and
bk_test_keys. Every JSON answer sayslivemode(trueat a live business,falseat a sandbox) and every answer theBookatu-Livemodeheader; a test key at a live business, or a live key at a sandbox, is403withcode: "LIVEMODE_MISMATCH", and a sandbox-only call at a live business answerscode: "NOT_A_SANDBOX". - The optional request header
Bookatu-Livemode: false(ortrue) says which kind of business a call means to reach: a call to a business of the other kind is403withcode: "LIVEMODE_MISMATCH"before anything runs, so a front end on the keyless endpoints built against a sandbox can never book at a live business. Browsers may send it. - Sandboxes are made, listed, reset and deleted with the developer account API,
/api/developer/sandboxes, signed in with a Bookatu login (never an API key); making one needsacceptTestModeTerms: true. A sandbox's own test key withbookings:writemay reset that sandbox, so CI needs no login. GET /sandbox/outboxlists what a sandbox would have sent (kept, never sent), andPOST /sandbox/simulate/{event}makesdeposit.paid,payment.failed,checkout.expired,booking.client_cancelled,booking.no_showorreminders.duehappen to one of its bookings. Both answer at a sandbox only, with its test key.- Every webhook payload has a top-level
livemodefield, and every delivery theX-Bookatu-Livemodeheader; a connected app's token answer sayslivemodetoo.
Booking questions
2026-10-30 · Release 14
questionsonGET /services,GET /services/{id}andGET /business: the questions a business asks when someone books.answersonPOST /bookandPOST /visits/book. A required question left unanswered is422withcode: "ANSWERS_REQUIRED"andmissingQuestionIds; an answer of the wrong type or length is422withcode: "ANSWERS_INVALID"andinvalidAnswers. Nothing is booked.- A service that cannot be booked online is refused as an unknown one before its questions are checked, so a hidden service's questions are never named.
Surcharges on public holidays and busy times
2026-10-30 · Release 14
- A slot at a time the business charges more for carries
priceCents(the price before the surcharge),surcharges(label,amountType,amount,amountCents) andtotalCents; every other slot has none of these fields. - A booking made at such a time is priced at its
totalCents, and its answer carriessurcharge: { label, amountCents }. The business MCP server's availability tools say the same.
Card holds and booking terms on the keyless doors
2026-10-30 · Release 13
- A service the business requires a card saved to hold is refused on
POST /bookandPOST /visits/bookwith409andcode: "CARD_REQUIRED_USE_BOOKING_PAGE", with thebookingPageUrlto send the person to; nothing is booked. - A business that has set booking terms needs
acceptTerms: trueonPOST /bookandPOST /visits/book; without it the answer is422withcode: "TERMS_NOT_ACCEPTED"and the terms to show.
Website widget events, version 1
2026-10-30 · Release 13
- The website widget dispatches
bookatu:<event>browser events andbookatu_<event>dataLayer events (ready, service_selected, staff_selected, time_selected, booked, request_sent, handoff, error, closed) with a payload markedv: 1, carrying a booking reference rather than the appointment id; deep links (data-date,data-promo,data-lang,data-staff,data-branch), a pinned loader/embed.v1.jsand an allow-list of the sites that may frame it.
Manage keys: the appointment id alone no longer changes a booking
2026-10-29 · Release 12 · Breaking
- A booking made from this release on carries a manage key:
POST /bookandPOST /visits/bookanswermanageKey, and the client's emailed links carry it ask. - The
/booking/{id}calls take it as?k=or theX-Manage-Keyheader. With the id alone,GET /booking/{id}answers a read-only status (access: "view"), and availability, reschedule and cancel answer403withcode: "MANAGE_KEY_REQUIRED". A booking made before keeps being managed on its id alone.
localTime on the keyless doors
2026-10-29 · Release 12
POST /book,POST /visits/bookandPOST /booking/{id}/rescheduletakelocalTime(with an optionaltimeZone) instead ofstartAt; aTIME_NEEDS_OFFSETrefusal now points to it.
Webhook delivery and OAuth refresh tokens
2026-10-29 · Release 12
- Every webhook delivery, retry and test event resolves the endpoint's name and is refused if any of its addresses is loopback, private, link-local or otherwise reserved, then connects to the address it checked. Redirects are never followed: a
3xxis a failed delivery. - A connected app's code is spent, and its refresh token rotated, in one step: a refresh token used twice is a replay, and the whole token family is revoked.
Rate limits enforced by default
2026-10-29 · Release 11 · Breaking
- The API, MCP and booking limits are enforced unless the platform's
API_RL_MODEsays otherwise; until then they only logged. A limit breached is a429withRetry-After. POST /api/oauth/registerhas limits of its own whatever the mode: 20 registrations per address a day and 2,000 a day across everyone.
The business MCP server: protocol versions, annotations, times and get_business
2026-10-29 · Release 11
- The business MCP server negotiates
2025-06-18,2025-03-26and2024-11-05, answersping,-32601for an unknown method and anynotifications/*with202and no body (never counted against the key). Tools carry titles and annotations, read tools anoutputSchemaandstructuredContent; with a 2025MCP-Protocol-Versionheader a tool's own refusal is a result withisErroranderror.code. get_business;timeZoneandWhenobjects on the reads;create_bookingandreschedule_bookinganswerwhen. Public text carrying someone else's email or phone is refused withPUBLIC_TEXT_CONTAINS_CONTACT_DETAILS.get_availabilityandget_visit_availabilityanswer compactly:timeZoneonce, each slot'slocaltime, and an optional, strictly checkedclientTimeZone.
Connected apps see client contacts only by consent
2026-10-29 · Release 11
- A connected app gets
customers:readonly when the owner ticks the client-contacts box on the consent page. Connections approved before keep exactly what they had.
The keyless booking doors hardened
2026-10-29 · Release 10 · Breaking
POST /bookandPOST /visits/booktake at most 5 bookings per address per business in 10 minutes and 15 a day, and 40 a day per business from every address together, enforced unless the platform'sKEYLESS_RL_MODEsays otherwise.- A service that takes a deposit is refused with
409andcode: "DEPOSIT_NEEDS_BOOKING_PAGE"andbookingPageUrl;payLinkis no longer in the answer ofPOST /book. startAtis read strictly, as RFC 3339 with its offset: a time with no offset is422TIME_NEEDS_OFFSETwith an example, anything else unreadable422TIME_INVALID. A paused business answersBOOKING_PAUSED.
Times and zones: when, localTime and clientTimeZone
2026-10-29 · Release 10 · Breaking
GET /availabilityandGET /visits/availabilitygive each slot awhenobject and saytimeZoneanddatesAreOn, and takeclientTimeZone.POST /bookandPOST /visits/booktakeclientTimeZone(an abbreviation or offset is422UNKNOWN_TIME_ZONE) and answerwhen;GET /booking/{id}answerswhen.POST /appointments,POST /visitsandPOST /visits/{id}/rescheduletakelocalTimeand refuse a time with no offset; a wall-clock time the clocks skip isTIME_DOES_NOT_EXISTwithnextValid, one they show twiceTIME_AMBIGUOUS.
Unpaid deposit holds
2026-10-29 · Release 10
- A booking held until its deposit is paid cannot be moved by the client:
POST /booking/{id}/rescheduleanswers409withblocked: "deposit_unpaid". An unpaid hold is released when its Checkout expires.
Visits, the checkout hand-off and opt-in scopes
2026-10-22 · Release 5
- The
/visitsendpoints: a visit of several services, booked, read, moved and cancelled as one (GET /visits/availability,POST /visits/book, and the keyed/visitscalls). - The opt-in scopes
checkout:read,checkout:writeandaudit:read, carried only by a key made with them ticked: for another point of sale checking bookings out, and for the audit log. - The opt-in
booking.checked_outwebhook event.
Scoped API keys
2026-10-22 · Release 4
- Each key has scopes, picked when it is made; every key-protected endpoint and MCP tool needs one, and a call outside the key's scopes is
403naming themissingScope. A key made before scopes keeps its access level.
Rate limits on the API and the MCP server
2026-06-19 · Release 4 or earlier
- Per-plan limits on the v1 REST API and the MCP server (per business, per key on the MCP server, per address and business on the public endpoints), with
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, andRetry-Afteron a429. Logged rather than enforced (the platform'sAPI_RL_MODEdefaulted to log) until release 11.