# Set up SimplyBook.me API access for AI coding agents

## Overview

This file is written for **you, the AI agent** (Claude Code, Cursor, Codex, Kiro, Copilot, etc.). The human who gave you this link is "the user". Follow the steps below to connect the user's project or AI assistant to SimplyBook.me — an online appointment booking platform — and to leave behind a rules file so that future sessions use the API correctly.

You run the commands yourself, explain briefly what each step does, and stop only where this file tells you to wait for the user.

Related files:

- Rules file for the project: [rules.md](rules.md)
- Troubleshooting by step: [troubleshooting.md](troubleshooting.md)
- Legacy JSON-RPC integrations, overview: [json-rpc.md](json-rpc.md)
- Legacy JSON-RPC public API: [json-rpc-public.md](json-rpc-public.md)
- Legacy JSON-RPC admin API: [json-rpc-admin.md](json-rpc-admin.md)
- Admin REST API reference (OpenAPI 3 JSON): https://simplybook.me/api/swagger-admin
- Public REST API reference (OpenAPI 3 JSON): https://simplybook.me/api/swagger-public
- Human-readable reference: https://simplybook.me/en/api/developer-api/tab/rest_api and https://simplybook.me/en/api/developer-api/tab/public_rest_api

## Before you start

### Choose the scenario

Ask the user which of these they want, unless it is already clear from the conversation or the project:

| Scenario | What it is | Go to |
|---|---|---|
| **A. Build an integration** | Code that calls the SimplyBook.me **REST API** — a custom booking flow, a sync with a CRM, reports, a mobile app, a backend job. | [Scenario A](#scenario-a--build-an-integration-with-the-rest-api) |
| **B. Manage the business from an AI assistant** | Connect the SimplyBook.me **admin MCP server** so the assistant can manage bookings, clients, services and schedules on the user's behalf. | [Scenario B](#scenario-b--connect-the-admin-mcp-server) |
| **C. Let an AI agent take bookings for clients** | Connect the SimplyBook.me **public MCP server** to a client-facing agent (chatbot, voice agent) that books appointments. | [Scenario C](#scenario-c--connect-the-public-mcp-server) |

For new work use the **REST API**. The **JSON-RPC API** (`https://user-api.<domain>`, methods like `getToken`, `getUserToken`, `getEventList`, `book`) is the legacy API. It still works: if the user's project already uses it, keep it working and follow [json-rpc.md](json-rpc.md) instead of this file's Scenario A. Do not mix the two APIs in one call.

### Required inputs

Collect these from the user (ask only for what you cannot find in the project):

1. **Company login** — the SimplyBook.me account name, e.g. `mycompany` in `mycompany.simplybook.me`.
2. **API host** — the region the account is registered in. The user can copy it from the admin panel: **Custom Features → API → REST API Endpoint**. It looks like `https://user-api-v2.<domain>/`. Common values:

   | Region | REST API host |
   |---|---|
   | Default / international | `https://user-api-v2.simplybook.me` |
   | Europe | `https://user-api-v2.simplybook.it` |
   | Asia | `https://user-api-v2.simplybook.asia` |
   | US data residency | `https://user-api-v2.simplybook.us` |

   Enterprise and partner accounts use other domains — always prefer the value shown in the user's admin panel. Do not guess the region — ask the user for the value from the admin panel. The wrong regional host returns `Invalid company`.

### Input constraints

- You MUST NOT ask the user to paste passwords, API keys, secret keys or tokens into the chat. Ask them to put secrets into an environment file or secret store themselves, and read them from there.
- You MUST NOT write secrets into source code, commit them, or print them in full in your output.
- You MUST NOT put admin credentials, API User Keys or the company **secret key** into browser, mobile or other client-side code. Only the **public REST API key** (used via `/public/auth/token`) is meant for client-side use; the public MCP key is not.
- You MUST NOT create, change or cancel real bookings, clients or settings without the user's explicit confirmation. Bookings send real notifications to real clients.

### Prerequisites

- `curl` (or PowerShell `Invoke-RestMethod` on Windows) to check connectivity.
- An active SimplyBook.me account. The user must be an administrator to enable custom features and create API keys.

---

## Scenario A — Build an integration with the REST API

### Step A1. Choose the API surface

The REST API has two parts. Pick one with the user based on who the end user of the integration is:

| | **Public REST API** | **Admin REST API** |
|---|---|---|
| Acts as | A client of the business (like the booking website) | A staff member / administrator |
| Typical use | Custom booking page, widget, mobile app for clients | Back-office sync, CRM/ERP integration, reports, staff tools |
| Paths | `/public/...` | `/admin/...` |
| Credential | Public REST API key | API User Key (recommended) or staff login + password |
| Runs where | Client-side allowed | **Server-side only** |
| Reference | https://simplybook.me/api/swagger-public | https://simplybook.me/api/swagger-admin |

An integration may use both (for example a public booking flow plus a server-side webhook handler that reads booking details through the admin API).

### Step A2. Get the credentials

Tell the user exactly what to create, then wait until they confirm it is done.

**Public REST API:**

1. In the admin panel, enable **Custom Features → API** (enabling **Smart Widget** also enables it).
2. Open the API feature settings and copy **"Your public REST API key"**.

**Admin REST API (recommended: API User Key):**

1. In the admin panel, open the **API User Keys** page (linked from the **API** custom feature settings; the address ends with `/v2/r/#/api-keys`) and create a key for the staff user the integration should act as.
2. The integration logs in with that user's **login** and the key as the **password**. The key starts with `api_user_key_`.
3. Logging in with an API User Key does not require two-factor authentication or email IP verification, so it works for unattended jobs. A plain staff password does require them when they are enabled, which makes it unsuitable for automation.
4. The integration gets the permissions of that user — suggest a dedicated user with only the permissions the integration needs.
5. Warn the user before they create the **first** API User Key on a live account: it also turns on email IP verification for password logins of that company (it stays on if the key is deleted). Other integrations that log in with a staff password will then need a key too.

If the credentials are already in the project (an env file or secret store), check that they are present without printing them and continue.

Then create an environment file that the user fills in, and make sure it is git-ignored:

```dotenv
# .env — never commit this file
SIMPLYBOOK_API_HOST=https://user-api-v2.simplybook.me
SIMPLYBOOK_COMPANY=<company login>
# Public REST API
SIMPLYBOOK_PUBLIC_KEY=<public REST API key>
# Admin REST API
SIMPLYBOOK_ADMIN_LOGIN=<staff user login>
SIMPLYBOOK_ADMIN_API_KEY=<api_user_key_...>
# Secret key (API custom feature settings): webhook signatures and JSON-RPC md5 signatures — server-side only
SIMPLYBOOK_SECRET_KEY=<secret key>
```

Keep only the variables the integration needs (a client-side app needs neither the secret key nor admin keys). If the project already names its settings differently, keep its naming. Use `SIMPLYBOOK_API_HOST` for every call. The `servers` list inside the OpenAPI spec only names the main production hosts; ignore it.

If the project already has a secrets mechanism (Vault, Doppler, cloud secret manager, framework config), use that instead.

### Step A3. Get a token and verify access

Calls made **with** a token send two headers:

```
X-Company-Login: <company login>
X-Token: <token>
```

- The login calls (`POST /public/auth/token`, `POST /admin/auth`, `POST /admin/auth/refresh-token`) take the company in the JSON body; `X-Company-Login` is not needed there (it does no harm if your HTTP client sends it by default).
- Do not use `Authorization: Bearer` with the REST API — it is ignored.

**Public REST API** — token is valid for 1 hour:

```bash
curl -s -X POST "$SIMPLYBOOK_API_HOST/public/auth/token" \
  -H "Content-Type: application/json" \
  -d "{\"company\":\"$SIMPLYBOOK_COMPANY\",\"key\":\"$SIMPLYBOOK_PUBLIC_KEY\"}"
# → {"id":null,"company_login":"...","token":"..."}
```

**Admin REST API** — token is valid for about 1 hour (the response has no expiry field):

```bash
curl -s -X POST "$SIMPLYBOOK_API_HOST/admin/auth" \
  -H "Content-Type: application/json" \
  -d "{\"company\":\"$SIMPLYBOOK_COMPANY\",\"login\":\"$SIMPLYBOOK_ADMIN_LOGIN\",\"password\":\"$SIMPLYBOOK_ADMIN_API_KEY\"}"
# → {"token":"...","refresh_token":"...","company":"...","login":"...","domain":"...","require2fa":false,"allowed2fa_providers":[],"auth_session_id":"","id":null}
```

`POST /admin/auth/refresh-token` with `{"company", "refresh_token"}` returns a **new** `token` and a **new** `refresh_token`; a refresh token works only once, so store the new pair.

Verify with a read-only call:

```bash
curl -s "$SIMPLYBOOK_API_HOST/public/services" \
  -H "X-Company-Login: $SIMPLYBOOK_COMPANY" -H "X-Token: $TOKEN"
# admin: curl -s "$SIMPLYBOOK_API_HOST/admin/services" -H ...
```

**Success:** `{"data": [...services], "metadata": {...}}`. Show the user the service names (not the token).

If `require2fa` is `true`, the user logged in with a password instead of an API User Key — go back to Step A2. For other errors see [troubleshooting.md#step-a3-getting-a-token](troubleshooting.md#step-a3-getting-a-token).

Tip for `curl`: URLs with `filter[...]` need `curl -g` (or URL-encoded brackets), otherwise curl fails with `bad range in URL`.

### Step A4. Save the API reference into the project

Download the OpenAPI spec(s) for the surface(s) in use, so you and future sessions read endpoints and field names from the source instead of guessing:

```bash
mkdir -p docs/simplybook
curl -s https://simplybook.me/api/swagger-public -o docs/simplybook/openapi-public.json
curl -s https://simplybook.me/api/swagger-admin  -o docs/simplybook/openapi-admin.json
```

If the project uses typed clients, offer to generate one from the spec (for example with `openapi-generator` or `openapi-typescript`). Do not invent endpoints that are neither in the spec nor in this file. Parse responses tolerantly: ignore fields you do not know, and respect fields the spec marks as nullable.

### Step A5. Implement the core flow

#### Public booking flow

Implement it step by step and test each call before moving on:

1. **Company settings:** `GET /public/config` — read `timezone`. Dates and times you send and receive for bookings and slots are in the **company timezone**, formatted `YYYY-MM-DD` and `YYYY-MM-DD HH:MM:SS`, without an offset.
2. **Services:** `GET /public/services` (filters: `filter[search]`, `filter[provider_id]`, `filter[category_id]`, `filter[location_id]`).
3. **Providers:** `GET /public/providers?filter[service_id]=<id>`.
4. **Free slots:** `GET /public/timeline/slots?service_id=<id>&provider_id=<id>&date_from=YYYY-MM-DD&date_to=YYYY-MM-DD` → `[{"date", "slots": [{"time", ...}]}]` (a plain array, not paginated). For "any provider", omit `provider_id` (do not send `provider_id=any` unless you also pass `location_id`; without it the result can be empty). The range of one request is at most 45 days (`400 Too long period`); this limits the size of a slots request, not how far ahead a booking can be made — the booking horizon is the company's "max time till event" setting. `GET /public/timeline/first-available-day?service_id=<id>&provider_id=<id>&start_datetime=YYYY-MM-DD%20HH:MM:SS` (`start_datetime` optional, default: now) returns a date string as a hint — it can be a day without free slots, so always confirm it with `/timeline/slots`.
5. **Check what the company requires** before building the booking request:
   - **Captcha:** `GET /public/captcha/challenge`. `{"provider": null}` → no captcha (the other fields are then empty). Otherwise the booking needs a `captcha_token` produced by the **end user** in the integration's UI: load the SimplyBook.me captcha widget script `captcha-widget.js` from the company's booking site (`https://<company booking site>/v2/js/lib/captcha/captcha-widget.js`; the exact URL is returned as `widgetUrl`), render `new SBCaptcha({container, challenge: <response>, onToken})`, and send the token from `onToken` as `captcha_token`. The widget works with every captcha provider the company can choose, so do not integrate reCAPTCHA/Turnstile/etc. directly. Ready-made snippets (REST, JSON-RPC, React, Vue, Angular) are in the admin panel: **Anti-Fraud/Spam Protection** settings → **Integration guide** (shown when **Enable captcha for API integrations** is on) — ask the user to copy the one for your stack if needed. A token is single-use — fetch a new challenge after a failed booking. An integration with no client-facing UI cannot get a token: tell the user to switch off **Enable captcha for API integrations** in the **Anti-Fraud/Spam Protection** custom feature settings, or to book through the admin API. Never try to solve or bypass the captcha programmatically.
   - **Client login:** if the **Client login** custom feature is on, bookings need a client token. Log the client in with `POST /public/clients/login` `{"login": <email>, "password": ...}` (or register with `POST /public/clients/register`, which creates a real account — ask first) and send that token as `X-Token`; then omit `client` in the booking body. The client id comes from `GET /public/clients`.
   - **Terms:** `GET /public/clients/terms` shows which terms are enabled (`enabled_*`). Show the enabled ones to the client and send those they accepted, e.g. `"terms": {"user_terms": true}` (`user_terms`, `simplybook_terms`, `cancellation_terms`, `privacy_policy`). `promotion_letters` is optional marketing consent — send it only if the client opted in. To record or change the client's acceptance explicitly, use `PUT /public/clients/terms`, and read it back with `GET /public/clients/terms`.
   - **Intake form:** `GET /public/additional-fields?filter[service_id][]=<id>`. Each field has `name` (a hash-like key, the one you send) and `field_name` (the label shown to people). Send values as `"additional_fields": [{"field": "<name>", "value": "..."}]`.
6. **Book:** `POST /public/booking/item` with `service_id`, `provider_id`, `start_datetime` (one of the returned slots), `count` (default 1), the client (`"client": {"name", "email", "phone"}` when Client login is off) and the items from step 5. The response has `bookings[]` with `id`, `code`, `is_confirmed` — `is_confirmed: false` is normal when the company approves bookings manually.
7. **Read / cancel:** `GET /public/booking/item/<id>` (optional `?hash=<client hash>`; without it the token's client must own the booking, otherwise `403`) and `DELETE /public/booking/item/id/<id>?reason=<text>`, with the same client token if Client login is on. A client's own bookings: `GET /public/booking` with a client token (without one → `403`). By default it lists only confirmed bookings; `filter[confirmed_only]=0` adds bookings waiting for approval **and** cancelled ones — check `status`. Spelling differs: `status` is `"cancelled"` here, `approve_status` and the admin API use `"canceled"`.

Before the first real `POST`, ask the user for permission to create a test booking and agree on a test service. Prefer a free service without memberships, group minimums or required payment. Cancel the booking afterwards.

#### Admin flow

- Company timezone: `GET /admin/company/info` → `timezone` (or `GET /admin/config` → `company_timezone`). `/public/config` does not accept an admin token. All booking date fields, including `record_date` (when the booking was created), are in company time.
- Providers: `GET /admin/providers?filter[service_id]=<id>`; their working days: `GET /admin/schedule?service_id&provider_id&date_from&date_to` (`is_day_off`).
- Slots: for a date range use `GET /admin/timeline/slots?service_id&provider_id&date_from&date_to` (same shape as the public timeline); for one date `GET /admin/schedule/available-slots?service_id&provider_id&date&count` → `[{"id": "YYYY-MM-DD HH:MM:SS", "date", "time"}]` (send `id` as `start_datetime`); `GET /admin/schedule/first-available-slot?service_id&provider_id&date&count` → one such object, or `null` when nothing is free. A specific provider and `count` are required. Admin bookings skip some public restrictions (such as minimum time before booking), so check slots yourself.
- Clients: find with `GET /admin/clients?filter[search]=<email>` and compare the email exactly (search is fuzzy); create with `POST /admin/clients` only if missing. Check `GET /admin/clients/fields` for required fields (`is_optional: false`) — the phone is often required. Custom client fields are written separately with `/admin/clients/field-values`.
- Booking: `POST /admin/bookings` with `service_id`, `provider_id`, `client_id`, `start_datetime`, `count`, `additional_fields`. Admin bookings are confirmed immediately. If the service is sold through memberships, the API answers `data.client_membership_id: ["Membership is required"]`: pass the client's `client_membership_id`, or `skip_membership: true` only if the business agrees.
- Booking list: `GET /admin/bookings?filter[date_from]&filter[date_to]&page&on_page` (both dates inclusive). Without filters it returns every booking ever made; it includes cancelled bookings. `filter[status]` takes one value (`confirmed`, `confirmed_pending`, `pending`, `canceled`); to skip cancelled bookings, filter `status != "canceled"` in your code.
- Cancel: `DELETE /admin/bookings/<id>` → `status: "canceled"`.

#### Lists and errors

- Most lists take `page` (default 1) and `on_page` (default 10, values above 100 are cut to 100) and return `{"data": [...], "metadata": {"items_count", "pages_count", "page", "on_page"}}`; loop until `pages_count`. Some endpoints return plain arrays or values (`/public/timeline/slots`, `first-available-day`, `first-available-slot`) or `{"data"}` without `metadata` (`/public/plugins`); the spec shows which.
- Errors return `{"code", "message", "data", "message_data"}`. For `400` the `message` is often empty — the reason is in `data` (field → list of errors). Log the whole body.

### Step A6. React to changes with webhooks (optional)

If the integration must react to new, changed or cancelled bookings or clients, do not poll — subscribe to webhooks through the admin REST API:

```bash
curl -s -X POST "$SIMPLYBOOK_API_HOST/admin/webhooks" \
  -H "Content-Type: application/json" \
  -H "X-Company-Login: $SIMPLYBOOK_COMPANY" -H "X-Token: $TOKEN" \
  -d '{"url":"https://example.com/simplybook/webhook","event":"new_booking"}'
```

Events: `new_booking`, `change_booking`, `cancel_booking`, `new_client`, `change_client`, `delete_client`, `new_invoice`, `new_offer`. At most 5 webhooks per event stay active: creating a 6th silently deactivates the oldest one (it is not deleted). Only create subscriptions for a URL the user controls, after asking.

The payload only carries IDs (for bookings: `booking_id`, `booking_hash`, `company`, `notification_type`), plus `webhook_timestamp` (Unix seconds) and `signature_algo` when signing is on. Fetch details with the admin API.

- Verify `X-Signature`: lowercase hex HMAC-SHA256 with the **secret key as the key** and the **raw request body as the message**, compared in constant time, computed on the raw bytes before parsing JSON. Requests are signed when the company has a secret key (`signature_algo: "sha256"`).
- `webhook_timestamp` is inside the signed body: after the signature check, reject values older than about 5 minutes to block replays.
- Retries can deliver the same event again: deduplicate, for example on `booking_id` + `notification_type` + `webhook_timestamp`.
- Answer `2xx` within 5 seconds from a public HTTPS URL without redirects, and do the work asynchronously. Failed deliveries are retried after 10 min, 30 min, 1 h, 3 h, 12 h and 24 h.

### Step A7. Write the rules file

Go to [Write the rules file](#write-the-rules-file).

---

## Scenario B — Connect the admin MCP server

The admin MCP server gives an AI assistant the same access as the user who approves it: bookings, clients, services, providers, schedules, custom features and reports.

1. Ask the user to enable **Custom Features → MCP** (Beta) in the admin panel and open its settings.
2. Ask the user to copy the **admin connector URL** from the MCP feature settings. Do not guess or construct this URL — always use the value shown there.
3. Add the server to the AI tool as a remote **HTTP** MCP server. It uses OAuth 2.1 with discovery — there is no key to paste. For example, in Claude Code:

   ```bash
   claude mcp add --transport http simplybook <admin connector URL>
   ```

   In other tools, add a remote/streamable-HTTP MCP server with that URL in their MCP settings.
4. **Wait for the user** to finish the browser sign-in and approve access when the tool prompts for it.
5. Verify: list the available tools and call a read-only one (for example the company info tool). Show the user the company name.
6. In the MCP settings in the admin panel, the user can click **Download instructions** to get the SimplyBook.me skill for the assistant. Suggest installing it — it teaches the assistant the correct order of operations and terminology.

Access ends when the user revokes the assistant in the MCP settings, changes their password, or disables the MCP feature.

---

## Scenario C — Connect the public MCP server

The public MCP server exposes only the client-facing booking tools (services, providers, available times, booking). Use it for an agent that talks to the business's clients.

1. Ask the user to enable **Custom Features → MCP** (Beta) and open **Public agent integration** in its settings.
2. The user copies the **public connector URL** and the **public MCP key** from these settings (the "API key" under **Public agent integration**) into the agent's configuration / secret store. Do not guess or construct the URL. The key does not expire; the user can rotate it with **Generate**.
3. Configure the agent's MCP client with the URL and the header `Authorization: Bearer <public MCP key>`. For example, in Claude Code:

   ```bash
   claude mcp add --transport http simplybook-public <public connector URL> \
     --header "Authorization: Bearer $SIMPLYBOOK_PUBLIC_MCP_KEY"
   ```

4. Verify: list the tools and fetch the list of services.
5. The agent must collect the client's name and email (and phone if the business requires it) before booking, and must only offer times returned by the availability tools.

The public MCP key is not the public REST API key: it does not work on `/public/auth/token` or the REST API, and the REST key does not work on MCP. Keep the public MCP key server-side, inside the agent's configuration — do not embed it in a public web page.

---

## Write the rules file

Fetch [rules.md](rules.md) and add its content to the rules file of every AI tool used in the project:

| Tool | Rules file |
|---|---|
| Claude Code | `CLAUDE.md` |
| Codex, other `AGENTS.md`-compatible tools | `AGENTS.md` |
| Cursor | `.cursor/rules/simplybook.mdc` |
| Kiro | `.kiro/steering/simplybook.md` |
| GitHub Copilot | `.github/copilot-instructions.md` |

- Append; never overwrite an existing file. Wrap the content in markers so it can be updated later:

  ```
  <!-- BEGIN simplybook-api-rules -->
  ...
  <!-- END simplybook-api-rules -->
  ```

  If the markers already exist, replace only the content between them.
- Existing project instructions take precedence over these rules. If they conflict, tell the user.
- If you cannot append safely (unknown format, generated file), show the user the content and ask where to put it.

## Done

Summarise for the user: the scenario, the API host and company login used, where the credentials are stored, which files you created or changed, and what was verified. Do not repeat secrets.

If something failed, see [troubleshooting.md](troubleshooting.md). For questions the docs do not answer, point the user to the developer forum: https://tech-support.simplybook.me/
