# Booked — developer documentation ## Start here # Architecture and integration map > What runs where, which extension points to use, and the behaviours that surprise developers: reminders, cron, and bookings that exist before the money. Source: https://trybooked.co/help/#dev/architecture Booked is a single WordPress plugin with a React admin, a React booking widget, and a licensing and payments platform it talks to over HTTPS. If you are integrating with it, this page is the map: what runs where, what you are allowed to change, and what will bite you. ## The shape of it | Piece | What it is | | --- | --- | | Admin app | A React single-page app on one WordPress admin screen. All navigation happens inside it — there are no WordPress submenus. | | Booking widget | A separate React bundle rendered by shortcode on the public site. | | REST API | About 200 routes under get-booked/v1. The booking form's and customer account's endpoints are open; the rest need a team member's session or an API key. | | Database | Several dozen custom tables, prefixed getbooked_. Bookings are not posts. | | Platform | Licensing, managed payments, AI and usage metering, reached over outbound HTTPS. | > **Note — Bookings are custom tables, not a post type.** There is no WP_Query for appointments, no post meta, and no admin list table to filter. Read through the REST API or query the tables directly, and expect neither to be a stable public contract. ## Where to hook in Pick the lightest tool that does the job — in this order. - **Booking form settings** — Colours, layout, steps and per-form CSS/JS are all configurable without code. Exhaust these first. - **Shortcode attributes** — Pre-selecting a service or branch, or rendering a specific styled form, is an attribute rather than a build. - **Hooks and filters** — A small set of actions and filters cover reacting to bookings and status changes, sending texts, printing tracking markup and supplying TURN servers. - **The public API** — Only when you are replacing the booking front end wholesale. ## What you must not do - **Do not edit plugin files. Booked checks its licensing files against a signed manifest; a changed file disables the booking form on the live site, and every update overwrites your edits anyway. Do not collect card details yourself. The card fields at the card step come from Booked Payments' hosted script, for PCI reasons. Do not run one licence on two hostnames. The licence is domain-locked and a clone will fight production for it. Do not poll the admin REST API for changes. Hook get_booked_booking_event instead. Two behaviours that surprise people Reminders depend on cron Appointment reminders are on by default — one the day before, and the merchant can change or add more under Messages. A background sweep checks every fifteen minutes for bookings entering a reminder's window. On top of the seven lifecycle events — pending, approved, cancelled, rescheduled, changed, refunded, paid — each reminder is its own event (reminder, reminder_2 and so on), and intake_chase chases unanswered questions. If reminders arrive late or not at all, WordPress cron is the first thing to check. Texts go nowhere by default The SMS provider setting defaults to none. Booked still counts segments the moment it wants to send, so an unconfigured site accrues usage while delivering nothing. See Sending SMS through your own gateway. Booking lifecycle Worth internalising before you build anything against it — the appointment exists before the money does. The booking is created** — POST /public/bookings. No card involved yet. - **Payment is confirmed separately** — POST /public/confirm-payment verifies the charge against the existing booking. - **Status moves** — To confirmed, or to pending if the merchant approves bookings by hand. - **The event fires** — get_booked_booking_event — your integration point. The practical consequence: a customer who abandons at the card step leaves a real, unpaid appointment behind. Any integration that assumes "booking exists therefore paid" is wrong. ## Environment expectations - **Outbound HTTPS to the platform is required.** There is no offline mode; the licence lapses without contact. - **WordPress cron drives scheduled work**, so it only runs when someone visits. Use a real cron on quiet sites. - **Aggressive asset optimisation breaks the widget.** Exclude Booked's script handles from combining and deferring. Each of these is covered properly in **Staging, migrations and environments**. --- # Staging, migrations and environments > The domain-locked licence, cloning safely, cron, and what deleting the plugin destroys. Source: https://trybooked.co/help/#dev/environments Booked's licence is locked to one domain, and the signed permission it holds expires if the site cannot reach the platform. That combination makes staging sites, migrations and firewalled hosts the most common way a developer breaks a working install. This article covers what actually happens and how to work without tripping it. ## How the licence binds to a site On activation the licence records the site's domain, with `www.` stripped, and the site receives a signed grant it re-checks periodically. Two rules follow: - **The domain must match. A copy of the database on a different hostname is not licensed, whatever the key says. The site must be able to reach the platform. The grant expires after fourteen days without contact. A site behind a firewall that blocks outbound HTTPS will eventually stop working even though nothing about it changed. > **Warning — What the merchant sees.** The booking form stops rendering. Visitors see nothing where it was; only a logged-in administrator sees a notice explaining why. Bookings already in the database are untouched — this is a rendering and API gate, not data loss — but the site cannot take new bookings until the licence is healthy. Cloning to staging The failure mode to avoid is a clone that activates itself and steals the domain lock from production. Work in this order: Clone as normal** — Copy files and database to the staging hostname. - **Deactivate the licence on the clone** — Under Settings › Your plan, remove the key from the Licence key card. The staging site keeps all its data and simply will not render the booking form. - **Develop against the rest of the plugin** — Everything that is not gated — the admin screens, your hooks, your theme integration — still works. - **Never point staging at live payments** — Managed payments belong to the licensed production domain. Do not attempt to take real cards from a clone. If you need a fully working staging site including the booking form, that is a second licence rather than a trick — ask the merchant to raise it with Booked rather than trying to run one key on two hostnames. ## Moving a site to a new domain - **Change the domain on the Booked account first** — The lock is held by the platform, so it has to be released there before the new hostname can activate. - **Move the site** — Files, database, and WordPress's own Site Address settings. - **Re-activate under Settings › Your plan** — Enter the key again on the new domain. - **Check the booking form renders** — Load a page carrying the shortcode as a logged-out visitor. ## Firewalled and air-gapped hosts The site makes outbound HTTPS calls to the platform for licence checks, managed payments, AI features and usage metering. If the host restricts outbound traffic, the licence will lapse on its own schedule. There is no offline mode — allowlist outbound HTTPS to the platform, or the install is not viable. ## Background jobs Booked leans on WordPress cron, which only runs when someone visits the site. On a quiet site scheduled work drifts or is missed for the day — most visibly appointment reminders, which are checked every fifteen minutes and can arrive late or not at all. Follow-ups, waiting-list offers and the team digest drift the same way. On any site where timing matters, disable WordPress's visitor-driven cron and drive it from the system instead. ```php // wp-config.php define( 'DISABLE_WP_CRON', true ); ``` ```shell # every five minutes */5 * * * * cd /var/www/site && wp cron event run --due-now >/dev/null 2>&1 ``` ## Updating the plugin Booked checks its licensing files against a signed manifest. Editing one of them, or a deployment that rewrites them, trips the tamper check and disables the booking form. Edits anywhere else are simply lost on the next update. Everything a developer needs is available through hooks, shortcodes and the code fields on a booking form — build there, and updates stay uneventful. ## Uninstalling > **Warning — Deleting the plugin destroys the data.** Deactivating is safe and reversible. Deleting runs the uninstaller, which drops the core tables — every booking, customer, note, service, member of staff, location and message template — and removes the Booked role and capabilities. Take a database backup before removing it from any site you did not personally build yesterday.It is not a clean sweep. Newer tables (packages, API keys, follow-ups, sent messages, secure documents, the waiting list, unfinished bookings) and almost every get_booked_ option are left in the database. Drop those by hand if you need the site truly clean. --- ## Building on Booked # The public booking API > The unauthenticated endpoints the booking widget runs on — build your own front end against these. Source: https://trybooked.co/help/#dev/public-api Booked ships one genuinely open API: the endpoints its own booking widget runs on. They need no authentication, no key and no nonce, because the people using them are anonymous customers. If you want to build your own booking front end — a React app, a native app, a bespoke page — this is the surface to build against. > **Note — This is an unauthenticated write path.** Management routes need a team member's session or an API key. /public/bookings creates a real appointment from an anonymous request. Booked throttles it — ten booking attempts per IP address per ten minutes, shared with document uploads — and checks a honeypot field, but add your own spam protection on any busy custom front end. ## Base URL and endpoints Everything sits under the get-booked/v1 namespace, in the public group. ```base https://yoursite.co.uk/wp-json/get-booked/v1/public/ ``` | Endpoint | Method | What it does | | --- | --- | --- | | /public/catalog | GET | Everything bookable: services, categories, locations, employees, plus branding and formatting. | | /public/availability | GET | Free start times for one service on one date. | | /public/availability-range | GET | The same across a span of days, in one call. | | /public/bundle-availability | POST | Slots where a set of back-to-back services all fit. | | /public/bookings | POST | Creates the appointment. | | /public/confirm-payment | POST | Confirms a card payment against a booking. | | /public/intake | GET | Fetches a screening form by its token. | | /public/intake-check | POST | Evaluates answers against the eligibility rules. | | /public/intake-submit | POST | Stores completed answers. | | /public/intake-upload | POST | Uploads an ID or document, returns a token. | | /public/triage | POST | Answers the routing questions one step at a time, when the form asks them first. | | /public/waitlist | POST | Joins the waiting list for a full day. | | /public/waitlist-offer | POST | Reads a waiting-list offer. | | /public/waitlist-claim | POST | Takes the offered slot. | | /public/progress | POST | Records how far a visitor got, for the Unfinished list. Write-only. | | /public/conversion | POST | Claims a booking's conversion payload for tracking. Only one caller ever gets it. | Packages have their own open routes (`/public/packages` and friends), and so does the customer account (`/public/account/…`) — see **Customer portal**. ## Reading the catalog One call returns the whole bookable world. Pass `variant` to get a specific booking form's configuration, or omit it for the default. ```http GET /wp-json/get-booked/v1/public/catalog GET /wp-json/get-booked/v1/public/catalog?variant=v_8k3ma2qd ``` The response carries `services`, `categories`, `locations` and `employees`, alongside store-level values you will need to render prices and times correctly — `currency` (an object: `code` and `symbol`), `time_format`, `timezone`, `brand_color`, the `company` block and the booking form's `form` settings. `bundles`, `waitlist` and `video` are on/off flags for those features, not lists. Each service carries the fields that decide how it can be booked: `duration`, `price`, `deposit` (an object: `required`, `amount`, `label`), `group`, `course`, `is_video`, `intake`, plus `location_ids` and `employee_ids` telling you where and with whom it can happen. > **Note — Respect the link fields.** location_ids and employee_ids are the whole reason a service is or is not offered somewhere. Filter on them before you ask for availability, or you will request slots that can never exist. ## Asking for times `/public/availability` takes the service, the date, and optionally a staff member and a party size. A branch (`location_id`) is required for in-person services and ignored for video ones. Leave `employee_id` off and Booked considers everyone qualified. ```http GET /public/availability ?service_id=12 &location_id=3 &date=2026-09-14 &party=1 // 200 { "date": "2026-09-14", "slots": [ { "time": "09:00", "employee_id": 3, "iso": "2026-09-14T09:00:00+01:00" }, { "time": "09:20", "employee_id": 5, "iso": "2026-09-14T09:20:00+01:00" } ], "timezone": { /* … */ }, "earliest": "…" } ``` Each slot is an object. Send its `employee_id` with the booking, or `0` to let Booked choose. When a day comes back empty, `notice_message` explains whether it is simply too soon to book. For a date picker, `/public/availability-range` answers for many days at once rather than making you loop. ## Creating a booking A POST to `/public/bookings` creates the appointment. It is created before any card is taken — payment is confirmed separately, against the booking that already exists. ```javascript const res = await fetch( '/wp-json/get-booked/v1/public/bookings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { service_id: 12, location_id: 3, employee_id: 0, // 0 = let Booked choose date: '2026-09-14', time: '09:20', first_name: 'Oliver', last_name: 'Ball', email: 'oliver@example.co.uk', phone: '07700 900145', party: 1, notes: '', website: '' // honeypot — must stay empty } ) } ); ``` A success returns the booking, its human reference and what is owed: ```json { "ok": true, "id": 318, "reference": "GB-00318", "status": "pending", "payment": { /* amount due, if any */ } } ``` | Field | Notes | | --- | --- | | employee_id | Send 0 to let Booked assign anyone qualified and free. | | party | Only meaningful on a service with group bookings switched on. | | website | A honeypot. Leave it empty; a filled value is treated as spam. | | course_visits | Chosen dates for a course, when the service runs over several visits. | | bundle_services | Extra services to run back-to-back in the same visit. | | intake / doc_tokens | Screening answers, and tokens returned by /public/intake-upload. | ## Taking payment If the service asks for money, the booking's `payment` object has `required: true`, the `amount` in minor units, a formatted `amount_display`, the `currency`, and whether the whole price is due (`full`). It also carries the values the card step needs to mount Booked Payments' hosted fields. - **If `redirect_url` is set,** send the customer there instead of showing card fields. - **Otherwise,** mount the hosted card fields, confirm the payment, then call `/public/confirm-payment` with the booking `id` and the returned payment reference (`payment_intent`). Booked verifies it and moves the appointment to confirmed. - **The server records the money anyway.** If the browser never calls back, payment is still recorded independently. > **Warning — Card fields are not yours to build.** Card details must never touch your own markup or your server. The card fields are supplied by Booked Payments' hosted script for PCI reasons — see Custom CSS, HTML and JavaScript for the content-security implications of blocking it. ## What is not open Everything for managing a business — bookings, customers, services, staff, settings, payments — needs either a signed-in team member (see **System › Team**) or a scoped API key. For server-to-server access, issue a key under **Settings › API keys** — see **API keys and the MCP server** and **Capabilities, roles and the admin API**. --- # Hooks and filters > Four actions and five filters, with signatures and working examples. Source: https://trybooked.co/help/#dev/hooks-filters Booked exposes four actions and five filters for developers. It is a deliberately small surface — enough to react to a booking, wire in a text gateway or tune video and group messages, without inviting people to rewrite the booking engine. Other `get_booked_` hooks exist in the code, but they are internal to Booked's own platform and can change without notice. Every hook name is prefixed `get_booked_`. ## Actions | Action | Arguments | Fires when | | --- | --- | --- | | get_booked_booking_event | $event, $booking_id | Any notification event: pending, approved, cancelled, rescheduled, changed, refunded, paid, each reminder (reminder, reminder_2…) and intake_chase. | | get_booked_booking_status | $from, $to, $booking_id | A team member changes a booking's status in the admin. Gives you the old and new status side by side. | | get_booked_send_sms | $to, $message | Booked wants a text sent. Booked SMS handles it when switched on; hook it to send through a gateway of your own. | | get_booked_pixels_body | — | Right after
opens, where tracking markup can be printed. | ### Reacting to a booking `get_booked_booking_event` is the one to reach for when another system needs to know. It fires for every notification event, reminders included, so switch on the event rather than assuming. ```php add_action( 'get_booked_booking_event', function ( $event, $booking_id ) { if ( 'approved' !== $event ) { return; } // Push the confirmed appointment into your own system. my_crm_sync( $booking_id ); }, 10, 2 ); ``` > **Note — Reminders are events too.** Booked sends appointment reminders itself, and each one fires get_booked_booking_event with its own key. If you only care about real changes to a booking, ignore keys that start with reminder and intake_chase. ## Filters | Filter | Receives | Controls | | --- | --- | --- | | get_booked_ice_servers | $servers (array) | The STUN/TURN servers used by video consultations. | | get_booked_room_permissions_policy | $policy (string) | The Permissions-Policy header the video room sends. Return an empty string to send none. | | get_booked_append_party_block | $append (bool), $channel, $booking_id | Whether a group booking's email or text gets the list of attendees added to the end. | | get_booked_client_ip | $ip (string) | The address used to work out a visitor's country. Useful behind a proxy that hides it. | | get_booked_visitor_country | $code (string) | A two-letter country code, for visitors Booked could not place itself. Feeds the Live view. | ### Changing the brand colour This is a setting, not a hook. The brand colour lives under **Messages › Design › Brand colour** and is used by branded emails and the video room. The booking form takes its colours from its own Style tab under **Set up › Booking form**. ### Leaving the attendee list off group messages If your message templates already name everyone, stop Booked adding the list a second time. ```php add_filter( 'get_booked_append_party_block', function ( $append, $channel, $booking_id ) { return 'sms' === $channel ? false : $append; }, 10, 3 ); ``` ### TURN servers for video Video consultations connect browser-to-browser. On restrictive corporate networks that fails without a TURN relay of your own. ```php add_filter( 'get_booked_ice_servers', function ( $servers ) { $servers[] = [ 'urls' => 'turn:turn.example.co.uk:3478', 'username' => 'booked', 'credential' => '…', ]; return $servers; } ); ``` ## Where to put this code A small site plugin is the right home — not the theme's `functions.php`, which stops running the moment someone switches theme, and not the Booked plugin itself, which is replaced on every update. ```php // wp-content/plugins/acme-booked-tweaks/acme-booked-tweaks.php /** * Plugin Name: Acme — Booked tweaks */ defined( 'ABSPATH' ) || exit; add_action( 'get_booked_booking_event', 'acme_on_booking', 10, 2 ); ``` > **Warning — Do not edit the plugin.** Booked checks its licensing files against a signed manifest. Editing them trips the tamper check and disables the booking form on the live site. Everything here is designed so you never have to. --- # Shortcode reference > All seven shortcodes, every attribute, and the script handles to leave alone. Source: https://trybooked.co/help/#dev/shortcodes Booked gives a merchant site eight shortcodes: four booking forms, a custom-built form, the customer account, the packages shop and the confirmation card. While the licence is inactive none of the forms, the account or the shop renders — visitors see nothing, and only a logged-in administrator sees a notice saying why. ## The eight you can use | Shortcode | Renders | | --- | --- | | [get_booked] | The full booking form, all steps. | | [get_booked_compact] | A condensed form for a sidebar or a narrow column. | | [get_booked_short] | A shortened flow, pinned to a service and branch. | | [get_booked_button] | A button that opens the form in a pop-up. | | [get_booked_custom] | A whole form built with the AI or custom form builder, by its id. | | [get_booked_account] | The customer account: appointments, payments, rescheduling. Needs Settings › Customer accounts switched on. | | [get_booked_packages] | The packages shop. Shows nothing until a package is on sale under Set up › Packages. | | [get_booked_confirmation] | The booking a customer has just made, drawn on a confirmation page of the site's own — the whole card, or one value with field. It reads the reference and signed key from the page address, which Settings › Booking rules › After they book (or a form's confirmation attribute) puts there. Nothing in the address, nothing drawn. See Your own confirmation page. | ## Attributes | Attribute | On | Default | Effect | | --- | --- | --- | --- | | service | the four forms | empty | Pre-selects a service and skips the service step. | | location | [get_booked_short] | empty | Pre-selects a branch. | | staff | [get_booked_short] | empty | Pins the appointment to one member of staff. | | next | [get_booked_short] | false | Offers a one-tap "next available" time. | | hide_price | [get_booked_short] | false | Hides the price. | | prequal | the four forms | empty | off skips the routing questions on this page. on cannot switch them on if routing is off for the site. Empty follows the site setting. On a variant form, routing always follows the Default form. | | variant | [get_booked] | empty | Renders a specific booking form by its id. | | full | [get_booked] | true | false narrows the layout. | | label | [get_booked_button] | Book now | The button's text. | | demo | the four forms, [get_booked_account] | false | Renders sample content instead of live data. | | id | [get_booked_custom] | empty | The custom form's id, like cf_ab12cd34. | | title | [get_booked_account], [get_booked_packages] | empty | A heading above the account or shop. | | service | [get_booked_packages] | empty | Shows only packages for that service. | | confirmation | the four forms, including variant | empty | Where this embed sends people once the booking, and any payment, is done — instead of the site-wide page. A path (/thank-you/) or a full address on this site; anything elsewhere is dropped. | | title, message | [get_booked_confirmation] | empty | Replace the heading and the line under it. {first_name} is substituted in both. | | show | [get_booked_confirmation] | empty (everything) | Comma-separated parts to draw: reference, service, staff, where, when, people, price, visits, video, questions, extras, account. | | fallback | [get_booked_confirmation] | empty | What a visitor with no booking in the address sees. Empty renders nothing. | | field, default | [get_booked_confirmation] | empty | One value instead of the card: first_name, name, email, reference, service, staff, where, address, date, time, end_time, duration, price, paid, due, people, visits, video_link, questions_link, manage_link. The three links render as links; default is printed when the value is empty. | | class | [get_booked_confirmation] | empty | An extra class on the .gb-conf wrapper. | `demo` accepts `true`, `1` or `yes`; `next` and `hide_price` also accept `on`. `full` is stricter: only `true`, `1` or an empty value keep the full layout, so `full="yes"` narrows it. ## Examples ```shortcode // The whole form, everything selectable. [get_booked] // A landing page for one service. [get_booked service="12"] // A specific styled form, by its id. [get_booked variant="v_8k3ma2qd"] // One service at one branch, shortest flow. [get_booked_short service="12" location="3"] // Sidebar widget. [get_booked_compact service="12"] // A call to action that opens the form over the page. [get_booked_button service="12" label="Book your flu jab"] // The customer account page. [get_booked_account title="Your appointments"] // A form that finishes on its own page. [get_booked confirmation="/thank-you-travel/"] // On that page: the card, trimmed, with its own heading. [get_booked_confirmation show="reference,when,where" title="See you soon, {first_name}"] // One detail inside your own sentence. Thanks [get_booked_confirmation field="first_name" default="so much"] — see you soon. ``` ## Where the ids come from Service and location ids are the numeric ids from Set up › Services and Set up › Locations. A form's `variant` id is shown with its shortcode under Set up › Booking form, and looks like `v_8k3ma2qd`. A custom form's shortcode is shown in the same place once the form is saved. Duplicating a form produces a new id, so a copied form needs its shortcode updating. ## Rendering in PHP To place a form inside a template rather than in page content: ```php echo do_shortcode( '[get_booked service="12"]' ); ``` > **Note — Keep it to one form per page.** Each shortcode mounts on its own, so two will both work. But two full forms on one page confuse customers and double your tracking events — a button alongside a form is usually what you want. ## Scripts and styles Assets load only on pages where one of the booking shortcodes actually renders, so an aggressive optimisation plugin that strips "unused" CSS can break the form. If you are deferring or combining scripts, exclude Booked's handles. A confirmation page carrying `[get_booked_confirmation]` loads the stylesheet and fonts but not the app script. | Handle | What it is | | --- | --- | | get-booked | The booking app itself — both the script and its stylesheet use this handle. | | get-booked-fonts | The widget's font stylesheet. | | get-booked-conversion | Fires conversion events into your pixels, when conversion reporting is on. | | the card script handle | Booked Payments' hosted card script, registered only when the site can take card payments. Read the exact handle from the page source — excluding it from script optimisation is what keeps checkout working. | The app also receives a `GetBookedPublic` object carrying `restUrl`, a REST `nonce` and the pixel configuration. If your caching layer serves a page with a stale nonce the booking request will still work — the public endpoints do not require one — but anything authenticated will not. --- # API keys and the MCP server > Issue a scoped key, call the API with it, and connect an AI assistant. Source: https://trybooked.co/help/#dev/api-keys An API key lets your own code — a script, an integration, or an AI assistant — read and manage a Booked site without a browser session. Keys are issued by the merchant, scoped to exactly what you need, and revocable in one click. ## Issuing a key - **Open Settings → API keys** — You need edit access to Settings, given under System › Team. An API key can never open this screen, so a key cannot mint or list other keys. - **Click New key and name it** — Name it after the thing using it — "Claude assistant", "Nightly export" — so it is obvious what breaks when you revoke it. - **Tick only what it needs** — Permissions are per area, split into Read and Write. A key with no Write anywhere cannot change a thing. - **Copy the key** — It is shown once and never again. Lose it and you make a new secret for the same key from the same screen; the old one stops working at that moment. > **Warning — Write access books real appointments.** A key with bookings:write can create, change and cancel appointments that staff and customers will act on, and a key with full control can rewrite services, questions, templates and settings. Issue read-only keys unless you specifically need to write, and grant only the areas the integration touches. ## Using it Send the key as a bearer token. Some hosts strip `Authorization` before PHP sees it, so Booked also accepts `X-Booked-Key` — sending both is the reliable move. ```shell curl 'https://yoursite.co.uk/wp-json/get-booked/v1/bookings' \ -H 'Authorization: Bearer gbk_8wscer4i_…' \ -H 'X-Booked-Key: gbk_8wscer4i_…' ``` ## Scopes A key's access comes from its scopes alone. The access of the person who created it is not lent to the key. A key made by an administrator with only `bookings:read` can read bookings and nothing else — it cannot reach settings, and it cannot write. Every area has a read and a write scope, and **Settings › API keys** offers two presets: **Read everything** and **Full control**. | Area | Scopes | Covers | | --- | --- | --- | | Appointments | bookings:read / :write | Bookings, availability, the waiting list, unfinished bookings, booking links, notes, documents, stats and insights. | | Customers | customers:read / :write | Customer records, their history and answers, and the block list. | | Services | services:read / :write | Services and their consultation questions, categories, packages, and the form's look. | | Locations | locations:read / :write | Branches and their hours. | | Staff | employees:read / :write | Staff, their hours, breaks and time off. Never their logins. | | Booking form | forms:read / :write | The form's steps, eligibility and routing questions, bundles and upsells, slot templates and custom code. | | Emails & texts | messages:read / :write | Every email and text template, reminders, follow-ups, and the log of what was sent. | | Settings | settings:read / :write | Company details, booking rules, features, import and export, and the activity log. | Tick every box and the key can do what the owner can do in Booked, with four exceptions that no scope reaches: it cannot list, make or revoke keys; it cannot invite, change or remove anyone on the team or touch a login; it cannot change the plan, billing or licence; and it cannot spend the assistant's own credits. Those routes answer `403` to any key, whatever it was issued with. Two rules follow from that, and both are enforced on the server: - **Reads need a `:read` scope, writes need `:write`.** A GET with only write scope is refused, and vice versa. - **An unmapped area is denied.** A key can never reach a part of the API its scopes do not name, and the four areas above are mapped to nothing. The refusal arrives before the route runs, as a `403` naming the permission to add. ## What you get back | Status | Means | | --- | --- | | 200 | Fine. | | 401 | No key, a malformed key, or one that has been revoked. | | 403 | The key is valid but lacks the scope for this area or method. | | 429 | Rate limited — 120 requests per key per minute. | ## Connecting an AI assistant Booked is an MCP server itself, so an assistant that speaks the Model Context Protocol can work with a site directly. There is nothing to install: point the assistant at the site's MCP address with a key, and it gets a set of tools rather than raw HTTP. **Settings › API keys** shows the exact address and step-by-step setup for Claude, Claude Code, ChatGPT, Cursor and Gemini CLI. ```json { "mcpServers": { "booked": { "url": "https://yoursite.co.uk/wp-json/get-booked/v1/mcp", "headers": { "Authorization": "Bearer gbk_8wscer4i_…" } } } } ``` ```shell # Claude Code claude mcp add --transport http booked https://yoursite.co.uk/wp-json/get-booked/v1/mcp \ --header "Authorization: Bearer gbk_8wscer4i_…" ``` Apps that only take a server address, with no headers, can carry the key in the address instead: `…/mcp?key=gbk_…`. Anyone who sees that address holds the key, so use a read-only key there. | Tool | Needs | | --- | --- | | list_services, list_locations, list_employees | the matching :read | | check_availability, list_bookings, get_booking | bookings:read | | create_booking, update_booking, cancel_booking | bookings:write | | list_customers | customers:read | | update_customer | customers:write | | get_service | services:read | | create_service, update_service — including a service's consultation questions (intake_form) | services:write | | get_booking_form | forms:read | | update_booking_form — including the eligibility and routing questions (triage) | forms:write | | list_message_templates | messages:read | | update_message_template | messages:write | | api — any management route, by method and path | that route's area, :read for GET and :write for the rest | | api_reference — the routes, what each takes, and the record shapes | nothing | **Everything else goes through `api`.** Staff and their hours, branches, categories, packages, follow-ups, booking links, the waiting list, settings, features: the assistant calls `api_reference` once to learn the routes and the shape of each record, then `api` with a method and a path such as `PUT employees/4`. A `PUT` or `PATCH` on a record changes only the fields it sends and the rest of the record is kept; `POST` creates; `DELETE` is permanent. The tool refuses anything outside the key's areas with the permission to add, and anything no key may reach with a sentence saying so. Each tool asks for its own scope, and the `api` tool asks for the scope of the route it is given. Every change lands in **Settings › Activity log** under the person who issued the key, named as made via API key "…", so a rewritten template or a moved appointment is never mistaken for the owner's own doing. The scopes do the enforcing, not the assistant's good behaviour: a tool the key is not scoped for returns a permission error however the model is prompted. Give an assistant a read-only key and it can answer questions about the diary but cannot touch it; give it full control and it can set the whole of Booked up for you, short of the team, other keys and the plan. > **Note — Check availability before booking.** The assistant should call check_availability and offer real times rather than inventing one. A time that is not returned there cannot be booked, and the attempt will simply fail. ## Looking after keys - **One key per integration.** Then revoking one does not break the others. - **Edit rather than reissue.** A key can be renamed or given more or less to do at any time; the change applies from its next request and nothing that holds the key has to change. Making a new secret keeps the key's id, name and permissions and invalidates the old secret at once. - **Check Last used.** A key that has never been used, or has not been used in months, should probably be revoked. - **Revoking is instant** and cannot be undone — the row stays so the audit trail survives, but the key stops working on the next request. - **Keys are stored hashed.** Nobody, including Booked, can read one back out of the database. That is also why we cannot show it to you twice. --- # Team permissions and the admin API > Team permissions, how the admin REST routes use them, and how scoped API keys sit on top. Source: https://trybooked.co/help/#dev/capabilities-rest Access to Booked is decided by team permissions, not WordPress capabilities. A WordPress administrator can do everything; anyone else has to be added under System › Team, with a level for each section. This article covers how that works, how the admin REST routes use it, where API keys fit, and — importantly — what you cannot do with it. ## Team permissions Each section of the app — the ones in the sidebar — can be set to **none**, **view** or **edit** for each team member. A scope can narrow that further: a branch manager sees only their own branches, a practitioner only their own appointments. Scope only ever narrows; it never grants. | Group | Sections | | --- | --- | | Overview | Dashboard, Diary, Live | | Appointments | /bookings, /availability, /waitlist, /abandoned, /booking-links, /notes, /documents, /stats, /analytics | Bookings, Diary, Unfinished, Booking links, Insights | | Customers | /customers, /blocklist | Customers | | Services | /services, /categories, /packages, /form-variants, /form-colors | Services, Categories, Packages | | Locations | /locations | Locations | | Staff | /employees | Staff (logins need Team) | | Booking form | /form-config, /form-custom | Booking form | | Emails & texts | /notifications, /messages, /flows | Messages, Follow-ups | | Settings | /settings, /features, /migration, /activity | Settings, Features, Payments | | Not open to keys | /api-keys, /team, /license, /account | Settings, Team — a person signed in, never a key | > **Note — Being on the team is the front door.** The Booked menu appears for WordPress administrators and for anyone added under System › Team. Nobody else can open Booked, whatever capabilities their role carries. Booked still adds five `gb_manage_*` capabilities and a `gb_employee` role on activation. Neither the menu nor the REST routes check them any more, so adding them to a role with `add_cap()` grants nothing. To give a shop manager the diary but not settings, add them under **System › Team** and set Diary and Bookings to edit, Settings to none. ## The admin REST routes A merchant site registers about 200 routes under `get-booked/v1`. Most are management routes; the open ones are covered in **The public booking API** and **Customer portal**. Each management route belongs to one section. Reading (`GET`) needs view on that section; anything else needs edit. **PUT replaces, PATCH changes.** A `PUT` on a record writes the whole record, so send every field: anything missing is written as its default, relations included. A `PATCH` changes only the fields you send and keeps the rest — the staff, hours and length of a service survive a rename. Prefer `PATCH` for any change that is not the whole record. (Before 2.23.2 the two behaved the same, and that was a bug.) | Area | Route group | Team section | | --- | --- | --- | | Bookings | /bookings, /notes, /documents | Bookings | | Customers | /customers | Customers | | Services | /services, /variants | Services | | Categories | /categories | Categories | | Locations | /locations | Locations | | Staff | /employees | Staff | | Messages | /notifications, /messages | Messages | | Activity | /activity | Bookings or Customers | | Settings, API keys | /settings, /api-keys | Settings | ## Authenticating a script Two ways in, and they are not equivalent. **API keys are the supported route.** A merchant issues one under Settings › API keys, scoped per area and per read/write. Its access comes from its scopes alone — the issuing user's team permissions are not lent to it. That is the mechanism to build against — see **API keys and the MCP server**. ```shell curl 'https://yoursite.co.uk/wp-json/get-booked/v1/bookings' \ -H 'Authorization: Bearer gbk_8wscer4i_…' ``` **WordPress Application Passwords** also work, because these are ordinary WordPress REST routes. They authenticate as the whole user, with every team permission that user holds — and the user must be an administrator or on the team — so they are the blunter instrument. | | API key | Application password | | --- | --- | --- | | Scope | Per area, per read/write | Everything the user's team permissions allow | | Beyond Booked | Cannot touch WordPress itself | Full REST access, posts and all | | Rate limited | Yes, per key | No | | Visible usage | Last used shown in the UI | Last used shown in the user profile | > **Note — Prefer an API key.** Reach for an application password only when you need something outside Booked in the same call. If you do use one, create a dedicated user, add them under System › Team with only the sections they need — never an administrator. Either way, these routes exist to serve Booked's own interface. They are not a versioned public contract and can change between releases; the **public booking API** is the stable surface. ## Reacting to changes instead If what you actually need is to know when something happens, polling the REST API is the wrong tool. Hook `get_booked_booking_event` and push the change outward — it fires in-process, needs no credentials, and cannot drift out of date. See **Hooks and filters**. ## Errors and reference codes Failures return a WordPress REST error with a machine code, and the customer-facing message carries a short reference such as `(ref: GB-SLOT)`. The underlying cause is written to Settings › Activity log under the Errors filter — when a merchant sends you a screenshot, that code is the lookup key. --- # The customer account portal > Give customers their own account — or build your own portal on the same API. Source: https://trybooked.co/help/#dev/customer-portal Your customers get an account: what they have booked, what they have paid, and — where you allow it — the ability to move or cancel an appointment themselves. Drop in a shortcode and it works. Or ignore the shortcode entirely and build the whole thing yourself against the API below, which is what the built-in one uses. > **Note — Nothing here works until it is switched on..** Customer accounts are off by default, and every capability on top is a separate switch. The API enforces both, independently of whatever your page decides to render — so a hidden button is a tidiness decision, never a security one. ## The five-second version ```shortcode [get_booked_account] ``` That renders the built-in portal — sidebar, spend totals, appointment history, rescheduling, cancellation — styled with your booking-form colours. It remembers which page it is on, so sign-in emails come back to the right place. Everything below is for when you want your own. ## How signing in works There is no password. A customer types the email address they booked with and gets a link that signs them in. | | What happens | | --- | --- | | 1 | They enter an email. You get the same answer whether or not it is on file. | | 2 | If it is known, a link is emailed. Single use, and dead after 15 minutes. | | 3 | The link returns to your page as ?gb_account=