---
title: "Changelog"
description: "Every change to the Bookatu v1 REST API, the MCP server and webhooks a developer needs to know about, newest first, each dated by the change that made it."
url: "https://bookatu.com/en-nz/developers/changelog"
language: "en"
section: "Developers"
---

# 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](https://bookatu.com/developers/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) with `endDate` (inclusive) or with `days`, instead of `startAt` or `localTime`. 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, without `staffId`) and the resources its service needs for every one of them, takes no extras and does not repeat. The answer has `startDate`, `endDate` and `days`.
- Refused with `422` naming the field, nothing booked: a time for a service of days, a date that is not real `YYYY-MM-DD`, no `endDate` or `days`, an `endDate` before `startDate`, `endDate` and `days` together, more than 60 days, a `repeat`, or any of the three for a service of minutes. `409` when the trip's person is already booked on those days, or no resource of a kind it needs is free on them.
- `GET /appointments` and `GET /appointments/{id}` carry `startDate`, `endDate` and `days` on a trip, after `notifyClient`; 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/payments` lists 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 its `outcome` (`paid`, `declined`, `abandoned`, `open`, `card_saved`, `refunded` or `partly_refunded`), `amountCents` and `currency`, `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 `kind` and `since`, and page with `limit` (1 to 100) and `cursor`, as on `GET /sandbox/outbox`; a filter that is not valid is `422` with `fieldErrors`. It takes a sandbox's test key with `reports:read`, the money reports' scope, and allows 30 calls a minute per sandbox and per key. A live business answers `404` with `code: "NOT_A_SANDBOX"`, and a key of the other mode `403` with `code: "LIVEMODE_MISMATCH"`.
- At a sandbox, the business MCP server's `tools/list` adds `sandbox_payments` after `sandbox_outbox`, for a credential with `reports:read`: the same payments, filters, cursor and limits, read-only and idempotent, with an `outputSchema`. A live business never lists it, and its `tools/list` is 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`) takes `notifyClient`. `false` makes 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 `true` or `false` is `422` with `fieldErrors.notifyClient`, and so is `notifyClient: false` with `requestDeposit: true`, since the pay link is sent to the client. Nothing is booked.
- `GET /appointments` and `GET /appointments/{id}` carry `notifyClient` on every booking, after `createdAt`. `POST /appointments/{id}/deposit-request` answers `409` with `reason: "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 take `notifyClient`: 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`, `busy` counts and each walk-in service with its `bookUrl`), 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 a `serviceId` that is not one of its walk-in services is 404 `SERVICE_NOT_FOUND`. Counts only: no client or team member is ever named.
- New `PUT /walk-in/manual` (`minutes` 0 to 240 or `notTaking: true`, and `expiry` `1h`, `2h` or `end_of_day`) and `DELETE /walk-in/manual`, for a key with `bookings:write`: the wait set by hand until it lapses, and back to the automatic wait (`cleared`). `PUT` answers 409 `WALK_IN_OFF` while 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 as `get_availability`, with `message` beside `status: "off"`), and `set_walk_in_wait` and `clear_walk_in_wait` (`bookings:write`).
- New error codes `SERVICE_NOT_FOUND` and `WALK_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_booking` and `create_visit` take an optional `answers` object, `{ [questionId]: answer }`: the person's answers to the business's booking questions, with the same shape, validation and limits as `answers` on `POST /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_services` lists each service's booking questions as `questions`, in the order they are asked, exactly as `GET /services` does. A business that asks none lists none, so its answer is unchanged.
- A required question left out is refused with `ANSWERS_REQUIRED` and `missingQuestionIds`, an answer that cannot be read with `ANSWERS_INVALID` and `invalidAnswers`, and nothing is booked, with `bookingPageUrl` beside them as before. The message now tells the agent to ask the person the questions and send their answers, or to send the person to `bookingPageUrl`.
- 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_booking` and `create_visit` no longer book a service (or a visit) that takes a deposit. They refuse it with `code: "DEPOSIT_NEEDS_BOOKING_PAGE"` and `bookingPageUrl`, the business's booking page (opened on the service for `create_booking`), where the customer books it and pays the deposit, as `POST /book` and `POST /visits/book` do. Nothing is written: no booking, no held slot, no client record and no message.
- The refusal is a tool error (`isError: true`, `structuredContent.error` with `code`, `message` and `bookingPageUrl`; no `livemode` there, as on every refusal: it is in `result._meta`) for a client that sends a 2025 `MCP-Protocol-Version` header, and the JSON-RPC error with `code` and `bookingPageUrl` in `error.data` otherwise. A sandbox answers the same, with its test key.
- There is no `payLink` in either tool's answer any more. Before this change such a booking was made `pending` and answered with a `payLink` to 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 REST `POST /appointments` (with `requestDeposit`) is unchanged.
- At a business whose booking page is not published yet, `bookingPageUrl` shows the customer a coming soon page until the owner publishes it, so the refusal there also carries `bookingPagePublished: false`, and its message says to publish the booking page first.
- Marked breaking, as the same change to `POST /book` was (2026-10-29), yet given to every version at once rather than held for a new one: the `payLink` opened 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/list` adds three tools after the rest, each only for a credential that may call it: `sandbox_outbox` (`customers:read`: the test outbox, as `GET /sandbox/outbox` answers it), `sandbox_simulate` (`bookings:write`: the six events of `POST /sandbox/simulate/{event}`, for the sandbox's own bookings only) and `sandbox_reset` (the sandbox's own `bk_test_` key with `bookings:write`, never a connected app). Each shares its REST twin's limits. A live business never lists them, and its `tools/list` is unchanged.
- Every successful tool result says `livemode` in `structuredContent` (`false` at a sandbox, `true` at a live business), and a write now answers with `structuredContent` too: its text's JSON plus `livemode`. The text block is unchanged. A refusal (`isError`) has no `livemode` in `structuredContent`; `result._meta["com.bookatu/livemode"]` says it on every tool result, and the `Bookatu-Livemode` header on every answer.
- Over MCP, a credential of the other mode than its business, or a call whose `Bookatu-Livemode` header names the other kind, is refused before anything runs: `403`, the JSON-RPC error `-32000`, with `error.data.code: "LIVEMODE_MISMATCH"`, `keyLivemode` and `businessLivemode`.
- 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`. Send `Bookatu-Version: 2026-10-30` to ask for it; every answer says the version it was answered in, in `Bookatu-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-Version` that is not a version the API answers in is refused before anything runs: `400` with `code: "UNSUPPORTED_API_VERSION"` and `supportedVersions`.
- Every answer of the v1 API and the MCP servers carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` for the window closest to its limit, and `RateLimit-Policy` naming every window the call was counted against; every `429` carries `Retry-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 says `livemode` (`true` at a live business, `false` at a sandbox) and every answer the `Bookatu-Livemode` header; a test key at a live business, or a live key at a sandbox, is `403` with `code: "LIVEMODE_MISMATCH"`, and a sandbox-only call at a live business answers `code: "NOT_A_SANDBOX"`.
- The optional request header `Bookatu-Livemode: false` (or `true`) says which kind of business a call means to reach: a call to a business of the other kind is `403` with `code: "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 needs `acceptTestModeTerms: true`. A sandbox's own test key with `bookings:write` may reset that sandbox, so CI needs no login.
- `GET /sandbox/outbox` lists what a sandbox would have sent (kept, never sent), and `POST /sandbox/simulate/{event}` makes `deposit.paid`, `payment.failed`, `checkout.expired`, `booking.client_cancelled`, `booking.no_show` or `reminders.due` happen to one of its bookings. Both answer at a sandbox only, with its test key.
- Every webhook payload has a top-level `livemode` field, and every delivery the `X-Bookatu-Livemode` header; a connected app's token answer says `livemode` too.

## Booking questions

2026-10-30 · Release 14

- `questions` on `GET /services`, `GET /services/{id}` and `GET /business`: the questions a business asks when someone books.
- `answers` on `POST /book` and `POST /visits/book`. A required question left unanswered is `422` with `code: "ANSWERS_REQUIRED"` and `missingQuestionIds`; an answer of the wrong type or length is `422` with `code: "ANSWERS_INVALID"` and `invalidAnswers`. 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.

## Client and booking tags

2026-10-30 · Release 14

- `tags` on `GET /customers`, `GET /appointments` and `GET /appointments/{id}`.
- `POST` and `DELETE` `/customers/{id}/tags` (the new opt-in `customers:write` scope) and `/appointments/{id}/tags` (`bookings:write`) put the business's tags on a client or a booking and take them off.
- The business MCP server's `list_customers` does not name tags; the keyed REST reads do.
- Webhooks: `campaign.sent`'s `audience` may now be `tag`, for a campaign sent to the clients with a tag, beside `all`, `recent`, `lapsed` and `vip`. It never says which tag (tags are the team's internal labels). A receiver that checks `audience` against a fixed list should add `tag`.

## 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`) and `totalCents`; every other slot has none of these fields.
- A booking made at such a time is priced at its `totalCents`, and its answer carries `surcharge: { 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 /book` and `POST /visits/book` with `409` and `code: "CARD_REQUIRED_USE_BOOKING_PAGE"`, with the `bookingPageUrl` to send the person to; nothing is booked.
- A business that has set booking terms needs `acceptTerms: true` on `POST /book` and `POST /visits/book`; without it the answer is `422` with `code: "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 and `bookatu_<event>` dataLayer events (ready, service_selected, staff_selected, time_selected, booked, request_sent, handoff, error, closed) with a payload marked `v: 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.js` and 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 /book` and `POST /visits/book` answer `manageKey`, and the client's emailed links carry it as `k`.
- The `/booking/{id}` calls take it as `?k=` or the `X-Manage-Key` header. With the id alone, `GET /booking/{id}` answers a read-only status (`access: "view"`), and availability, reschedule and cancel answer `403` with `code: "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/book` and `POST /booking/{id}/reschedule` take `localTime` (with an optional `timeZone`) instead of `startAt`; a `TIME_NEEDS_OFFSET` refusal 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 `3xx` is 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_MODE` says otherwise; until then they only logged. A limit breached is a `429` with `Retry-After`.
- `POST /api/oauth/register` has 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-26` and `2024-11-05`, answers `ping`, `-32601` for an unknown method and any `notifications/*` with `202` and no body (never counted against the key). Tools carry titles and annotations, read tools an `outputSchema` and `structuredContent`; with a 2025 `MCP-Protocol-Version` header a tool's own refusal is a result with `isError` and `error.code`.
- `get_business`; `timeZone` and `When` objects on the reads; `create_booking` and `reschedule_booking` answer `when`. Public text carrying someone else's email or phone is refused with `PUBLIC_TEXT_CONTAINS_CONTACT_DETAILS`.
- `get_availability` and `get_visit_availability` answer compactly: `timeZone` once, each slot's `local` time, and an optional, strictly checked `clientTimeZone`.

## Connected apps see client contacts only by consent

2026-10-29 · Release 11

- A connected app gets `customers:read` only 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 /book` and `POST /visits/book` take 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's `KEYLESS_RL_MODE` says otherwise.
- A service that takes a deposit is refused with `409` and `code: "DEPOSIT_NEEDS_BOOKING_PAGE"` and `bookingPageUrl`; `payLink` is no longer in the answer of `POST /book`.
- `startAt` is read strictly, as RFC 3339 with its offset: a time with no offset is `422` `TIME_NEEDS_OFFSET` with an example, anything else unreadable `422` `TIME_INVALID`. A paused business answers `BOOKING_PAUSED`.

## Times and zones: when, localTime and clientTimeZone

2026-10-29 · Release 10 · Breaking

- `GET /availability` and `GET /visits/availability` give each slot a `when` object and say `timeZone` and `datesAreOn`, and take `clientTimeZone`. `POST /book` and `POST /visits/book` take `clientTimeZone` (an abbreviation or offset is `422` `UNKNOWN_TIME_ZONE`) and answer `when`; `GET /booking/{id}` answers `when`.
- `POST /appointments`, `POST /visits` and `POST /visits/{id}/reschedule` take `localTime` and refuse a time with no offset; a wall-clock time the clocks skip is `TIME_DOES_NOT_EXIST` with `nextValid`, one they show twice `TIME_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}/reschedule` answers `409` with `blocked: "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 `/visits` endpoints: a visit of several services, booked, read, moved and cancelled as one (`GET /visits/availability`, `POST /visits/book`, and the keyed `/visits` calls).
- The opt-in scopes `checkout:read`, `checkout:write` and `audit: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_out` webhook 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 `403` naming the `missingScope`. 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-Remaining` and `RateLimit-Reset`, and `Retry-After` on a `429`. Logged rather than enforced (the platform's `API_RL_MODE` defaulted to log) until release 11.
