Skip to main content
Cloudbeds is the property management system (PMS) for hotels and hostels we build tools against. Its public docs are at developers.cloudbeds.com (an index for AI tools is at 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.
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

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

Authentication

Use a property API key as a Bearer token. There is no token exchange:
  • 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:
  • 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:
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: 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.