patchRate, and a rate manager that writes base rates 18 months
out and then has to prove each night actually moved. Cloudbeds’ docs are wrong
on several of these points.
Endpoints
Encoding: PHP-form fields, not JSON
The docs showrates=[{…JSON…}] in the form body. Cloudbeds rejects that with “The rates parameter should be a collection”. Send PHP-style nested fields:
Content-Type: application/x-www-form-urlencoded and Accept: application/json. Put one rates[] entry per rateID, with all its intervals nested under interval[].
rateID is the rate plan, not the interval
patchRate wants the rate plan’s ID (about 7 digits on our property), not an interval’s ID (about 10 digits). Sending an interval ID gives “RateID … does not exist”.
- There’s no API to list rate plans, and
getRatesis a quote, not a list. We read the plan IDs from the property’s Base Rates page in the Cloudbeds UI: the hidden<input name="rate_id">is the plan ID, next to<input name="room_type_id">. Each interval row’sdata-real_idis the interval ID, which you can’t use here. - Each room type gets one base rate plan automatically, holding all its intervals.
- Neither call creates a rate plan.
patchRateandputRateboth need an existingrateID;putRatewith an empty or0ID gives “rateID parameter value is missing”. Create the room types (and so their base plans) first.
Intervals
-
An interval is
{ startDate, endDate, rate }.patchRatealso takesminLosandmaxLos;putRatedoesn’t. -
patchRatecreates intervals on write. A date range that doesn’t overlap an existing interval becomes a new one. You don’t have to create intervals first. -
At most 30 intervals per call. Cloudbeds’ wording is “30 interval per update”. We chunk at 30 across the whole request, counting every
rateID’s intervals together, which is the conservative reading; whether the limit is really perrateIDis unconfirmed, and getting it wrong returns an error rather than truncating silently, which is the good failure. -
Day-of-week prices (
day_0…day_6) are silently ignored. We sent different prices per weekday and every day landed at the flatrate. To vary price by weekday, split the range into single-rate intervals yourself. - Interval names can’t be set. API-created intervals are named after their date range.
-
A derived rate can’t be written.
getRatereturnsisDerivedandparentRateIDat the top level.patchRaterefuses a derived rate, so read that flag before you build a plan rather than discovering it mid-batch against a live property.
Async jobs
patchRate and putRate return HTTP 202 with { "success": true, "jobReferenceID": "…" }. The change isn’t done yet: check with getRateJobs?jobReferenceID=…. Jobs are kept for 7 days.
A job-level error does not mean the whole batch failed
getRateJobs returns a job with status of in_progress, completed or
error, and a per-interval updates[] array. Each entry carries its own
startDate/endDate, an action of in_progress, updated, created or
error, the rate that was actually set, and a message.
error at the job level means one or more of those updates failed, not all
of them. Settle per interval, not per job: read job-level error as a batch
failure and a 30-night update with one bad night files 29 correct changes as
failures.
Four things we refuse to read as success, each of which otherwise records a
price nobody set:
- the job finished and reported nothing for a date we sent
actionis a value we don’t recogniseactionsaysupdated/createdbut carries no rate- the rate it reports is not the rate we asked for
Ordering is not documented, and updated is a claim about the past
Cloudbeds publishes no ordering guarantee for the queue, and its RMS guide
recommends sending simultaneous updates within the rate limits. So two sends
to the same date are not guaranteed to land in the order you made them.
More importantly, a job reporting updated says what Cloudbeds did, not
what the rate is. A night changed again five minutes later — by you, by
another integration, or by hand in the UI — reports updated for ever. If you
need to know the current price, read it back.
Prefer the webhook to polling
Cloudbeds’ RMS guide recommends subscribing to theapi_queue_task/rate_status_changed webhook rather than polling getRateJobs.
Polling works and is what we do, because it needs no public endpoint — but it
costs one upstream call per job and is bounded by whatever deadline the caller
is waiting behind.
Reading a rate back: getRate
getRate?roomTypeID=…&startDate=…&endDate=… returns data.roomRateDetailed[],
one entry per date, with rate and totalRate, plus roomsAvailable,
blocked, closedToArrival, closedToDeparture, minLos and maxLos. The
top level carries rateID, isDerived and parentRateID.
This is the endpoint that answers “what is this night selling at”, and it is
worth building on for three reasons:
- It settles whether a write stuck, which no job report can.
- A date with no interval is reported as absent, not as zero — but we have
not established that the API never returns a literal
0for one. Zero is also a real setting (a date closed out), so treat a missing figure and a zero as different things and never coerce either withNumber(x) || 0. - It is the check before a create. Writing a range you believe is empty is the difference between filling a gap and overwriting a year of prices, and this is the only way to verify the boundary.
Money comes back as a decimal. Converting to integer cents, round rather than
truncate:
86.9 * 100 is 8690.000000000001 in JavaScript, and truncating
loses a cent on every price.Finding the rate to sell at
A rate is a room type composed with a rate plan, and it has its own ID. Rate plan495538 is rate 3266901 under one 8-bed dorm and 3266902 under
another. postReservation wants the rate ID in rooms[].roomRateID, and the
wrong one fails with “No rate found. Please verify roomTypeID and/or
roomRateID”.
getAvailableRoomTypestakes no source parameter, yet silently applies each plan’s source restrictions. A negotiated rate the booker is entitled to use is simply absent from the response — active, in date, covering the room type, priced, and invisible. Read the plans separately and merge them in, or you will quietly sell at rack rate.- Query rate availability as
rooms=1&adults=1. Asking for the real party size hides every room type that could take part of the group, which is exactly what a multi-room booking needs to see. - The base rate plan may be UNNAMED rather than named
default. Matching the literal string finds nothing and every room type reports “no base rate”, which looks like a property misconfiguration rather than a classifier bug. roomTypeUnitscounts units, not rooms — beds for a dorm, rooms for a private type. Printed as “rooms” it reads as 144 rooms for an 18-room dorm block.isPrivateis not reliable. One property reportsfalseon every one of its private rooms. Decide room-level versus bed-level booking from theROOMSrecords instead.
Rates and reservations
A reservation is charged at the rate plan’s current rate when it’s created. Rates you load affect new bookings’ charges, so load them before creating reservations. A missing rate for a room type and dates makespostReservation fail with “No rate found”. See Reservations.