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