> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bellabooking.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API keys

> Give your own systems scoped, revocable access to your Bella business — read your catalogue, check availability, and create, reschedule and cancel appointments

## Overview

An API key lets a system you control talk to Bella on your business's behalf. The two things people build most often are:

* **A booking form on your own website** — read your services, check availability, then create the client and the appointment.
* **A sync into another system** — pull your clients and appointment history into a CRM, a data warehouse or a backup.

Appointments created through the API appear in your scheduler like any other, your clients get the same confirmation messages, and the Booking source report attributes them to **Integration** so you can tell them apart.

You create, edit and revoke keys yourself in **Settings → Integrations**. Nobody at Bella needs to be involved.

<Note>A key belongs to **one location**. If you run more than one location, switch to that location before creating its key. A key can never be moved to a different location.</Note>

## Quickstart

Four calls take a visitor from "what do you offer?" to a booked appointment.

```bash theme={null}
# 1 — what can be booked, and at what price
curl https://api.bellabooking.com/api/servicecategories \
  -H "x-api-key: $BELLA_API_KEY"

# 2 — when is it free
curl -G https://api.bellabooking.com/api/Appointments/availability \
  -H "x-api-key: $BELLA_API_KEY" \
  --data-urlencode 'startDate=2026-08-04' \
  --data-urlencode 'endDate=2026-08-09' \
  --data-urlencode 'teamMemberId=any' \
  --data-urlencode 'services=[{"serviceId":"6839...c1"}]'

# 3 — who is booking (creates, or returns the existing client)
curl -X POST https://api.bellabooking.com/api/Clients \
  -H "x-api-key: $BELLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Ava","lastName":"Nguyen","email":"ava@example.com","phoneNumber":"0412 345 678"}'

# 4 — book it
curl -X POST https://api.bellabooking.com/api/Appointments \
  -H "x-api-key: $BELLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "clientId": "6840...9f",
        "appointmentDate": "2026-08-05",
        "services": [{
          "serviceId": "6839...c1",
          "teamMemberId": "6837...a4",
          "startTime": "14:00:00",
          "price": 120,
          "durationInMinutes": 60
        }]
      }'
```

<Warning>
  **You supply the price and duration, and Bella records what you send.** They are not re-read from your catalogue at booking time.

  Always take `price` and `durationInMinutes` from the catalogue response in step 1 rather than hard-coding them, or a price change in Bella won't reach your website. See [Pricing your booking correctly](#pricing-your-booking-correctly).
</Warning>

## Authentication

Send the key in the `x-api-key` header on every request. You never need to say which location — the key already knows.

```bash theme={null}
curl https://api.bellabooking.com/api/servicecategories \
  -H "x-api-key: bella_live_7Kq2v9xR4mNp_a3F9…"
```

Keys are stored hashed, so nobody — including us — can read one back after it's created. If you lose it, revoke it and create another.

| Status | Meaning                                                                                                            |
| ------ | ------------------------------------------------------------------------------------------------------------------ |
| `401`  | The header is missing, malformed, or the key has been revoked or deleted                                           |
| `403`  | The key is valid but doesn't have the permission this endpoint needs, or API access isn't enabled for your account |

## Managing keys

<Steps>
  <Step title="Open Settings → Integrations">
    You'll need permission to manage settings. You can only grant a key permissions you hold yourself.
  </Step>

  <Step title="Name it after what will use it">
    "Website booking", "CRM sync", "Ops bot" — so the **Last used** column tells you something useful later.
  </Step>

  <Step title="Choose what it can do">
    Tick only what that integration actually needs. You can change this later without reissuing the key.
  </Step>

  <Step title="Copy the key">
    It's shown **once**. Store it as an environment variable on your server. If you lose it, revoke it and create another.
  </Step>
</Steps>

From the **⋯** menu next to any key:

* **Edit** — rename it or change its permissions. The key itself never changes, so whoever is using it doesn't have to do anything.
* **Revoke** — stop it working, keep the row so you can still see when it was last used. This cannot be undone.
* **Delete** — remove it from the list entirely. If the key was still active this also stops it working.

<Note>Edits, revocations and deletions all take effect **within a minute** rather than instantly — keys are cached briefly to keep requests fast.</Note>

## Permissions

| Permission                | Scope                 | Opens                                                        |
| ------------------------- | --------------------- | ------------------------------------------------------------ |
| Read services & prices    | `catalog:view`        | `GET /api/servicecategories`                                 |
| Check availability        | `availability:read`   | `GET /api/Appointments/availability`                         |
| Create a client           | `clients:create`      | `POST /api/Clients`                                          |
| Read client records       | `clients:view`        | `GET /api/Clients/search`, `GET /api/Clients/{id}`           |
| Create an appointment     | `appointments:create` | `POST /api/Appointments`                                     |
| Read appointments         | `appointments:read`   | `GET /api/Appointments/search`, `GET /api/Appointments/{id}` |
| Reschedule an appointment | `appointments:edit`   | `POST /api/Appointments/{id}/reschedule`                     |
| Cancel an appointment     | `appointments:cancel` | `POST /api/Appointments/{id}`                                |

Everything else returns `403`. A key cannot reach sales, reports, payments, team management, settings, or the API keys themselves — no key can ever create another key.

<Tip>Give each integration its own key with only the permissions it needs. A website booking form needs the four in the first block; a read-only CRM sync needs only the two view permissions.</Tip>

## API reference

Base URL: `https://api.bellabooking.com`

Requests and responses are JSON. All ids are strings. Dates are `YYYY-MM-DD`; times of day are `HH:MM:SS`.

### List the catalogue

```
GET /api/servicecategories
```

Returns your categories, each containing its services and bundles, with prices, durations, variants, pricing tiers and add-on groups.

```json theme={null}
{
  "data": [{
    "id": "6838...b7",
    "name": "Colour",
    "services": [{
      "id": "6839...c1",
      "name": "Full head colour",
      "price": 120,
      "durationInMinutes": 60,
      "disabledForOnlineBooking": false,
      "requiresMembership": false,
      "useVariants": false,
      "usePricingTiers": false
    }],
    "bundles": []
  }]
}
```

<Warning>
  This returns your **full** catalogue, including services you've switched off for online booking. Filter on `disabledForOnlineBooking` before showing anything to a visitor, or your website will offer services you've deliberately taken offline.

  Check `requiresMembership` too if you use members-only services — the API will book them for a non-member. The same applies to `prerequisiteServiceIds` / `prerequisiteRequirement` / `prerequisiteMatchMode` — the API does not enforce prerequisite gates.
</Warning>

### Check availability

```
GET /api/Appointments/availability
```

| Parameter       | Required | Description                                                        |
| --------------- | -------- | ------------------------------------------------------------------ |
| `startDate`     | yes      | First date to check, `YYYY-MM-DD`                                  |
| `endDate`       | no       | Last date to check. Defaults to `startDate`                        |
| `teamMemberId`  | yes      | A team member id, or `any` to let Bella pick whoever is free       |
| `services`      | yes      | JSON array of the cart, e.g. `[{"serviceId":"…","variantId":"…"}]` |
| `preferredTime` | no       | Biases slot ordering towards a time of day                         |

```json theme={null}
{
  "days": [{
    "date": "2026-08-05",
    "slotCount": 14,
    "timeSlots": [{
      "startTime": "14:00:00",
      "endTime": "15:00:00",
      "availableTeamMemberIds": ["6837...a4"],
      "availableTeamMemberNames": ["Priya"]
    }]
  }],
  "allowJoinWaitlist": false
}
```

Pass `variantId` and any `addons` in the cart where the service uses them, so the slots you get back match the real length of the appointment.

<Note>Availability reflects rosters, existing appointments and blocked time. It uses your **dashboard's** view rather than your booking page's, so it does not apply your minimum-notice period or gap-reduction settings — see [Booking rules](#booking-rules).</Note>

### Create or match a client

```
POST /api/Clients
```

| Field         | Required | Description                                                          |
| ------------- | -------- | -------------------------------------------------------------------- |
| `firstName`   | yes      |                                                                      |
| `lastName`    | no       |                                                                      |
| `email`       | no       | Required unless `phoneNumber` is given                               |
| `phoneNumber` | no       | Required unless `email` is given. Parsed for your business's country |

If the email or phone already belongs to one of your active clients, that client's id is returned instead of a duplicate being created — so a returning client filling in your form doesn't create a second record.

```json theme={null}
{ "id": "6840...9f" }
```

Either way you can go straight on to the booking with the id you get back.

### Create an appointment

```
POST /api/Appointments
```

| Field                          | Required | Description                                     |
| ------------------------------ | -------- | ----------------------------------------------- |
| `clientId`                     | yes      | From the previous step                          |
| `appointmentDate`              | yes      | `YYYY-MM-DD`                                    |
| `services`                     | yes      | One entry per service line                      |
| `services[].serviceId`         | yes      |                                                 |
| `services[].startTime`         | yes      | `HH:MM:SS`                                      |
| `services[].price`             | yes      | See the warning below                           |
| `services[].durationInMinutes` | yes      | See the warning below                           |
| `services[].teamMemberId`      | no       | Omit only for services that need no team member |
| `services[].variantId`         | no       | Required where the service uses variants        |
| `services[].bundleId`          | no       | Tags the line as part of a bundle               |
| `services[].addons`            | no       | `[{ "optionGroupId": "…", "optionId": "…" }]`   |

```json theme={null}
{ "id": "6841...2c", "warning": null }
```

#### Pricing your booking correctly

Bella records the `price` and `durationInMinutes` you send. It does not re-read them from your catalogue. That means:

* **Always read them from `GET /api/servicecategories` at booking time.** Hard-coded prices go stale the moment you change a price in Bella.
* **Where a service uses variants,** take the price and duration from the chosen variant, not the base service.
* **Where a service uses pricing tiers,** take them from the tier matching the assigned team member.
* **Add-ons** add their own price and duration on top of the service line.

Getting this wrong doesn't fail — it books at the price you sent, and that's what the client is charged at checkout.

#### Booking rules

Bookings made with an API key are recorded with the same checks your **front desk** gets, not the stricter ones your public booking page gets. Bella will accept a booking that:

* double-books a team member
* is in the past, or inside your minimum-notice period
* is with a team member who isn't qualified for that service
* is a members-only service for someone without a membership
* has unmet prerequisite services (a treatment that online booking would lock until a consult is booked or completed)
* would have needed your approval if it came through your own booking page

**Check availability first and only offer slots that came back from it.** That is what keeps a website booking honest.

<Note>Services that need a room or piece of equipment can't be booked through the API yet. Your own booking page assigns one automatically; the API doesn't, so it returns a clear error asking for a resource.</Note>

### Reschedule an appointment

```
POST /api/Appointments/{id}/reschedule
```

| Field             | Required | Description                    |
| ----------------- | -------- | ------------------------------ |
| `appointmentDate` | yes      | The new date, `YYYY-MM-DD`     |
| `startTime`       | yes      | The new start time, `HH:MM:SS` |

Every service line moves together, keeping the gaps between them. The client gets your usual reschedule message.

### Cancel an appointment

```
POST /api/Appointments/{id}
```

| Field              | Required | Description                       |
| ------------------ | -------- | --------------------------------- |
| `reasonId`         | no       | Your cancellation reason          |
| `cancellationNote` | no       | Free text kept on the appointment |

Packages, memberships, gift-card balances, loyalty points and promo codes the appointment used are restored automatically.

<Note>Cancelling through the API does not apply your cancellation-fee policy — no fee is charged and no policy-based refund is calculated, the same as a front-desk cancellation. If you charge for late cancellations, apply that in your own system or from the dashboard.</Note>

### Read clients

```
GET /api/Clients/search
GET /api/Clients/{id}
```

`search` accepts `filter` (name, email or phone), `limit`, `skip` and a range of filters including `createdAfter`, `createdBefore`, `lastAppointmentAfter` and `tags`.

### Read appointments

```
GET /api/Appointments/search
GET /api/Appointments/{id}
```

`search` requires `startDate` and `endDate`, and accepts `teamMemberIds`, `serviceIds`, `statusIds`, `clientFilter`, `limit`, `skip` and `orderBy`.

<Warning>
  Read endpoints return the **full internal record** — including private notes, client timeline notes and their attachments, card-on-file details and payment links.

  Never pass these responses straight through to a browser. Read them on your server, keep only the fields you need, and treat anything you store as you would the records in your dashboard.
</Warning>

<Note>There's no incremental cursor yet. A sync re-reads the date or page range it asks for. Deleted records leave no tombstone, so a full reconciliation is more reliable than a running total.</Note>

## Errors

Errors are returned as JSON with an HTTP status and a `detail` explaining what went wrong.

| Status | Meaning                                                                 | What to do                                        |
| ------ | ----------------------------------------------------------------------- | ------------------------------------------------- |
| `400`  | The request was invalid — a missing field, a bad id, a rule that failed | Read `detail`; don't retry unchanged              |
| `401`  | Missing, malformed or revoked key                                       | Check the header and that the key is still active |
| `403`  | The key lacks the permission this endpoint needs                        | Add the permission in Settings → Integrations     |
| `404`  | No such record in this location                                         | Remember a key only ever sees its own location    |
| `422`  | The request was understood but couldn't be completed                    | Read `detail`                                     |
| `429`  | Rate limited                                                            | Wait for `Retry-After` seconds, then retry        |
| `5xx`  | Something went wrong at our end                                         | Retry with backoff; if it persists, contact us    |

```json theme={null}
{
  "title": "Validation error",
  "status": 400,
  "detail": "An email address or a phone number is required."
}
```

## Rate limits

Limits are applied per key, per minute.

| Limit                                | Value                                                                                         |
| ------------------------------------ | --------------------------------------------------------------------------------------------- |
| Requests overall                     | 150 per minute, burstable                                                                     |
| Writes (anything that isn't a `GET`) | 5 per minute — [contact the Bella Booking team](mailto:hello@bellabooking.com) to increase it |
| Requests in flight at once           | 10                                                                                            |

Exceeding a limit returns `429` with a `Retry-After` header. Wait that many seconds and retry — don't retry immediately in a loop.

<Note>The write limit is sized for a real booking form rather than a bulk import — so if you're migrating data or running a busier site, ask us to raise it rather than working around it. These limits protect your business's own performance and may change; they aren't a service guarantee.</Note>

## Keeping your key safe

<Warning>Never put an API key in code a visitor's browser can read. Anyone who can see it can act as your business. Keep it on your own server and call Bella from there.</Warning>

* Store it as an environment variable or in a secrets manager, never in your source code.
* Give each integration its own key, so you can revoke one without breaking the others.
* Revoke a key the moment it's no longer needed, or the moment you suspect it's been exposed.
* Narrow a key's permissions when an integration stops needing something — editing is safer than leaving a key more capable than it has to be.
* Your key can read and change real client data. Treat it like the password to your dashboard.

[Tell us what you're building](mailto:hello@bellabooking.com) — it genuinely shapes what we build next.
