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

# Cloudbeds API

> What we've learnt calling the Cloudbeds property API: base URLs and versions, authentication, the response envelope, rate limits, and how to add to this section.

Cloudbeds is the property management system (PMS) for hotels and hostels we build tools against. Its public docs are at [developers.cloudbeds.com](https://developers.cloudbeds.com) (an index for AI tools is at [developers.cloudbeds.com/llms.txt](https://developers.cloudbeds.com/llms.txt)).

This section isn't a copy of those docs. It records **what we learnt by calling the API**: behaviour that is undocumented, documented wrongly, or only obvious after a failed live call. Read it before you build against Cloudbeds, and add to it when you learn something new.

> [!NOTE]
>
> Each finding says where it came from. **Confirmed live** means a real call against a real property proved it. Anything less certain says so. Property-specific values (IDs, payment method codes, product IDs) are examples from one property: look yours up again.

## Where these findings come from

| Build | Repo | What it does |
| - | - | - |
| Cloudbeds handover migration (T0E hub) | `worker-cloudbeds-migration` | Moved \~630 bookings, with their folios, payments, notes and rates, from one Cloudbeds property to another at an asset sale (June–July 2026). |
| FRB group bulk importer | `worker-frb-bulk-importer` | Creates one multi-bed group reservation from a spreadsheet: rate discovery, bed-level allocation, then `postReservation` (Sept–Oct 2026). |
| FRB invoicing | `worker-frb-finance-1` | Reconciles a trading partner's remittance statement against Cloudbeds bookings, raises the Xero invoice, then marks each reservation paid with `postPayment` (Sept–Oct 2026). |
| FRB rate management | `worker-frb-ratemanager` | Writes base rates 18 months out with `patchRate`, then confirms each night actually moved (Oct–Nov 2026). |

## Base URLs and versions

Cloudbeds serves the same API under more than one host and version, and **not every endpoint works everywhere**. These are the combinations we used successfully:

| Base URL | Used for |
| - | - |
| `https://hotels.cloudbeds.com/api/v1.2` | Reads: `getHotels`, `getReservations`, `getReservation`, `getReservationNotes`, `getReservationInvoiceInformation`, `getTransactions`, `getSources`, `getRoomTypes`, `getRooms`, `getCustomFields`, `getPaymentMethods` |
| `https://api.cloudbeds.com/api/v1.2` | All writes on reservations: `postReservation`, `putReservation`, `postGuest`, `postGuestsToRoom`, `postCustomItem`, `postPayment`, `postAdjustment`, `postReservationNote` |
| `https://api.cloudbeds.com/api/v1.3` | `getReservationsWithRateDetails` (a reservation with each room's `rateID`/`rateName`), `getRates`, `patchRate`, `putRate`, `getRateJobs` |

The accounting API (`https://api.cloudbeds.com/accounting/v1.0`, with `getFolios`, `listFolioTransactions` and `/folios/configurations`) authenticated with our key but returned **empty results** for our property: it wasn't on the accounting ledger. Don't count on it for folio work. See [Folios and payments](https://mcp.theoryofevolution.com.au/docs/api-reference/cloudbeds/folios-and-payments.md).

## Authentication

Use a **property API key as a Bearer token**. There is no token exchange:

```http theme={null}
Authorization: Bearer cbat_…
```

* When you create an API key, Cloudbeds also shows a client ID, client secret and redirect URI. Those are for its **OAuth 2.0** flow, for partner apps that need a user's consent. Property-level calls don't use them. Don't go down the OAuth `/access_token` path: it's a rabbit hole.
* The `propertyID` comes from the key. Read it with `getHotels`. One key can see more than one property; ours saw one each.
* **The key carries scopes, and a missing one fails at the call.** There is no
  endpoint that lists what a key may do, so a scope you did not ask for is
  discovered by the write that needs it being refused. Decide the full set when
  you create the key — adding one later means issuing a new key and rotating it
  everywhere. Writes we have needed: `write:rate` (`patchRate`/`putRate`),
  `write:reservation` (`postReservation`), `write:payment` (`postPayment`).
  Reads have their own `read:*` scopes.
* **Keys expire after 30 days without use.** If a build sits idle between a rehearsal and a cutover, call it at least monthly.
* Writes post as the key's **name**: custom items posted by an integration are grouped under a category with the key's name (ours was "Jason Imports"). Name the key deliberately, because it shows up in folio configuration.
* Keep keys in Worker secrets. Never put them in a repo or in these docs.

## Requests

* **Reads** are `GET` with query parameters.

* **Writes** are form-encoded:

  ```http theme={null}
  Content-Type: application/x-www-form-urlencoded
  Accept: application/json
  ```

* **Arrays and objects must be PHP-style nested form fields**, not JSON strings. `rooms[0][roomTypeID]=123&rooms[0][quantity]=1` works, while `rooms=[{…}]` is rejected. This applies to `postReservation` (`rooms`, `adults`, `children`), `postCustomItem` (`items`) and `patchRate`/`putRate` (`rates`). A small encoder that walks the object and writes `key[i][field]=value` pairs covers all of them.

* **Status changes use `PUT`** (`putReservation`).

## Responses

Every response is wrapped in an envelope:

```json theme={null}
{ "success": true, "data": … }
```

**HTTP 200 with `"success": false` is common**: invalid status changes, refused checkouts and validation errors all come back as 200. Always check `success` in the body, not just the HTTP status. On failure, read `message`. Some errors also carry `error_code` and `details`; for example, `MAX_ADULTS_EXCEEDED` includes `details.room_type_id`.

**A refusal can be a 200 that reads like an outage.** A suspended property
answers reads with `success: false` and a message naming the problem — *"the
property(ies) you are accessing have an invalid status … overdue: 24924"*. A
client that checks only the HTTP status sees an empty but successful import, and
every downstream comparison then reports real bookings as missing. Read
`message` and surface it: the one sentence that explains the outage is in the
body.

Response shapes vary between endpoints and versions. `getSources` returns `data` as an **array of arrays**, one inner array per property the key can see. Some endpoints return `reservationID` at the top level and others under `data`, so read both.

## Idempotency differs per endpoint, and it matters

There is no general idempotency header. Each write is its own case, and the
difference decides what a client may safely retry:

| Endpoint | Retrying the same payload | Why |
| - | - | - |
| `patchRate` / `putRate` | **Safe** | They *assign* a rate. The same payload produces the same rate, so an unknown outcome can be resolved by sending it again. |
| `postCustomItem` | **Safe with `referenceID`** | Cloudbeds rejects a second call carrying the same one. |
| `postReservation` | **Not safe** | It *creates*. Nothing upstream notices a duplicate booking; carry your own one-time claim, keyed on a hash of the request body. |
| `postPayment` | **Not safe** | It *adds*. A folio paid twice reads as a reconciliation that ran twice and settled twice. |

Build the retry policy around that table rather than around the HTTP status. A
timeout means the same thing in every row; what differs is whether you may act
on it.

## Rate limits

Cloudbeds allows **5 requests per second** per property and can **suspend credentials** if you keep breaking that limit. Space every call at least 250 ms apart (about 4 per second) with one shared throttle, and never fire calls in parallel with `Promise.all`. In practice the throttle, not Cloudflare's subrequest cap, limits batch size: migrating one booking took 6–12 calls.

## Adding to this section

When a build teaches you something about Cloudbeds:

1. Add it to the page for that area, or start a new page under `api-reference/cloudbeds/` and list it in `docs.json`.
2. Say how you know it: **confirmed live** (and roughly when), or what is still unconfirmed.
3. Add your build to the table above.
4. Keep keys, guest names and personal details out.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.