Skip to content

Rates, restrictions and quotes

In property settings (/w/:workspace/p/:property/settings/rate-plans), members with rates.configure_plans (built-in: administrators) configure room-type plans, their meal plan, booking policy, order and optional derivations. Rates and availability (/w/:workspace/p/:property/rates) shows ledger availability and effective nightly values in 7/14/31-night views. Members with rates.edit (built-in: administrators and revenue managers) can edit a price inline (double click/F2), select ranges (mouse or Shift+arrows), fill from the first cell, paste a matching TSV rectangle, and edit prices/restrictions with a weekday mask. Derived plans show their rule and remain read only. Saved changes offer a ten-second Undo; intervening rate writes make that Undo stale, so it cannot overwrite another operator. Property-scoped rates.read is granted to the built-in admin, front_desk, accounting, revenue_manager and viewer roles; housekeeping cannot read rates. A custom role must have the effective rates.read grant for the property. Overriding a selected quote’s price requires reservations.override_price (built-in: admin and revenue_manager). A denied action is 403; another workspace’s property or plan is unavailable (404).

All paths below follow /api/v1/properties/{property_id} and accept a session, device token or API key with the GuestCraft-Workspace-Id header.

Operation SDK Contract
GET /rate-plans ratePlans.list Optional include_archived=true; active plans by default
POST /rate-plans ratePlans.create rates.configure_plans; 201, Location and plan ETag
PUT /rate-plans/{id} ratePlans.update rates.configure_plans; complete input, plan If-Match
PATCH /rate-plans/{id}/archive ratePlans.archive rates.configure_plans; { archived }, plan If-Match; supports restoration
GET /rates?from=…&to=… rates.get ≤366 half-open nights, effective days, availability, calendar version and pending count
PUT /rates rates.apply Calendar If-Match; ≤20 base plans, ≤366 nights; returns version and exact Undo payload
POST /rates/restore rates.restore Calendar If-Match; validates/restores sparse cells, also used for rectangular paste
POST /quotes quotes.create Room type, arrival/departure, adults/children; every active plan with nights, total and refusal reasons
POST /rerates rerates.apply reservations.rerate; 1–100 selections, each a reservation at its previewed version with optional room_ids, half-open from/to and expected_total; validate_only=true previews and saves nothing; a stale selection answers 412 rerate.stale and nothing is saved (bulk re-rating)

The SDK methods accept generated options (path, body, query, headers, signal). No write is automatically retried. RatePlan, RateDay, RateWindow, BulkRatesInput, BulkRatesResult, RateRestore, QuoteInput and RateQuote are generated public types. The restore endpoint accepts up to 4 MiB so a complete 7,320-cell Undo fits; other routes retain the default 1 MiB limit. An explicit installation maxBodyBytes overrides both.

Plan inputs include sell_mode (per_room or per_person), occupancy, age_bands and package_items; see occupancy pricing, children and packages. Currency follows the property, whose currency cannot change while plans exist. Parent and room type must match; cycles, a chain of more than 32 plans and archiving a parent with active children return 409 rate_plan.conflict. A plan’s room type and base/derived mode cannot change after creation. Percentages use signed integer basis points; fixed adjustments use signed integer minor units. Every derivation has an explicit rounding mode and minor-unit increment. Negative or unsafe results become missing prices. Non-positive final prices are refused, including base nightly prices. Derived plans inherit all seven restrictions. The settings UI accepts percentage and property-currency amounts and converts them to wire units exactly.

Rates use decimal strings in the property’s currency. Restriction fields are min_stay_arrival, min_stay_through, max_stay (null means unlimited), closed_to_arrival, closed_to_departure, stop_sell and max_sell (null means no cap; see sales caps). Zero and null are different values everywhere: 0 sells nothing. An omitted patch field is preserved, while price: null deliberately clears its price. Calendar changes also enqueue all affected descendants. Setup changes enqueue 500 future nights from the property’s local date. Reservation and inventory changes enqueue room-type availability rows without a plan. Every write commits audit, event and ARI rows in the same transaction. pending_changes counts pending and claimed rows; it never claims that a channel received them.

New reservation requests quotes once room type, dates and occupancy are valid. Choosing a bookable plan fills the total, and the preview displays its individual nightly amounts. Refused plans explain CTA/CTD, arrival/through minimums, maximum stay, stop sell, missing prices, capacity, invalid occupancy or unsafe totals. CTA applies on arrival; CTD on the departure date; through minimums apply on every occupied night, while the maximum applies on arrival. Channex’s public ARI documentation does not specify how changing nightly maxima are evaluated across a stay; ADR 0027 records the chosen rule and its certification risk. Quotes do not persist or hold inventory.

A reservation room may send rate_plan_id, quoted_amounts (the canonical decimal strings of each quoted night), and price_overridden. Without an override its price field is optional; the server recalculates and freezes the quoted nights. A changed quoted amount returns quote.changed (412), so the operator reviews a fresh quote. An explicit override requires reservations.override_price and a complete price. Restrictions and capacity still apply. The property gate serializes rate edits and final booking checks. Repricing a room credits its existing sold capacity before checking availability. In-house edits keep contracted nightly amounts and only price added nights at current rates. Editing cannot silently remove a saved plan reference to bypass restrictions. Detail exposes rate_plan_id and price_overridden after reload, and displays the chosen plan and override label.

Existing manual-price bookings remain readable. Manual creation remains available when a room type has no active plans; new manual rooms carry the override label. Once plans exist, creation/repricing requires choosing a plan. Existing frozen nights do not change when future rates change. An occupancy-only API edit can retain its frozen prices, except on a plan priced by occupancy (per person, with age bands or with a per-guest package): there the frozen nights are those of the sold occupants, so an expected stay with other occupants needs a new quote (409 quote.required). In-house stays keep their frozen nights.

A plan sells per room (one price whoever stays) or per person. A per-person base plan has occupancy: { base, max, curve }: it sells every adult count from 1 to max (at most 18, the number of occupancy options Channex accepts, and at most the room type’s adults). The calendar’s nightly price is the price of base adults; the curve gives the others:

  • { "kind": "per_adult", "extra_adult": 3000, "fewer_adult": 2000 } adds 30.00 per adult above the base and takes 20.00 off per adult below it;
  • { "kind": "explicit", "adjustments": [-2000, 0, 2500] } adds one signed amount per adult count from 1 to max; the base count’s is 0.

Amounts are integer minor units of the property’s currency, like derivation.amount. A count whose result is not positive has no price on that night and cannot be sold. GET /rates adds occupancy_prices ([{ occupancy, price }]) to each day of a per-person plan, and the rates calendar shows them under the plan’s row. Derived plans declare no curve or bands: they use their root plan’s, and each occupancy is the derivation of the parent’s price for that occupancy.

age_bands prices children by their age on arrival: at most one infant band, starting at 0, and one child band above it, up to 17, each with a nightly_fee added per child and night (0 for free). These are the two fees Channex can carry; more bands, or an infant band above the child band, answer 422 rate_plan.not_distributable with the field, as does a max above 18. With bands, quotes and bookings need child_ages, one per child (child_ages_required); a child older than the last band is refused (child_age_unpriced) and is booked as an adult. Without bands, children stay free and ages are optional.

package_items lists up to ten catalog extras the price includes, each with a quantity and a basis: per_night, per_person_night (per adult and child) or per_stay (the first night). Quote and reservation nights carry package, the included services at their catalog prices when quoted; a night whose package is worth its whole price or more is refused (package_exceeds_price). The folio posts each served night’s accommodation and its package services apart (folios).

distribution says where a plan is sold: { "scope": "direct" } (never published to Channex, any local pricing), or { "scope": "channels", "channels": [...] } with booking_com, expedia, agoda, airbnb and hostelworld, none meaning any of them. A channel plan is held to what its channels represent (see the distribution matrix): Airbnb, and any channel when none is declared, takes one price up to the base and one constant amount per extra adult; Hostelworld one price per room; no channel is documented to charge a child fee. Its age bands must also give every age the category the property’s Channex hotel policy gives it, once GuestCraft has read that policy. A refusal names the field: occupancy/curve, derivation, distribution or age_bands.

A plan mapped to Channex, or the root of a mapped derived plan, keeps what its Channex plan holds: its sale basis, its highest adult count, its age bands (ages and fees) and its sale through Channex (409 rate_plan.channex_locked with the field); create a new plan with the new setting and map it. The curve’s amounts stay editable. A plan with active derived plans keeps its sale basis (422 rates.invalid on sell_mode). A published derived plan cannot move to a parent whose root holds other settings (rate_plan.channex_locked on derivation/parent_id).

On PUT, occupancy, age_bands, package_items and distribution may be left out: a member left out keeps its stored value, so a client of an earlier revision changes nothing it does not know; null or [] clears it explicitly, under the same rules.

Each quote answers a fingerprint of its whole offer, and a validate_only move or resize that re-quotes a room answers it on the room as quoted_fingerprint. Send it as quoted_fingerprint with quoted_amounts when booking, editing or moving with a quote: an offer that changed since, including only its included services or terms, answers 412 quote.changed. It is required when the quoted or sold nights include a package; only an offer without one confirms without it. A manual price (price_overridden) is saved as entered with the package of the offer quoted, so it sends that offer’s fingerprint under the same rule.

Frozen nights do not follow later rate changes. To apply new rates to stays that are already sold, members with reservations.rerate (built-in: administrators and revenue managers) open Rates → Re-rate stays (/w/:workspace/p/:property/rates/rerate). The link is hidden for members without the grant, and the screen says who can grant it.

  1. Choose the first and the last night (both included) and, optionally, a room type. The screen lists the stays, not cancelled, with a night in that range (at most 100; narrow the dates to see more).
  2. Select stays and press Preview difference. In a multi-room stay, open its room list to re-rate only some rooms (every room by default). Each stay shows its current total, its total at the current rates and the signed difference, every night whose price changes, and every room that keeps its prices with the reason. The total difference of the selection is shown below.
  3. Apply new prices asks for confirmation and applies exactly what the preview showed. If a selected reservation or its rates changed since the preview, nothing is saved: the screen explains it and offers Preview again.

Only the selected nights from the property’s first night still to come are re-rated: the business date, or the local date while the night audit lags. Every other night keeps its frozen price. Each night gets the current effective price of the room’s own rate plan, derived plans included. A room keeps every price, and the answer lists it in skipped, when its stay is over or cancelled (closed), it belongs to an OTA booking whose channel sets the price (channel_managed), it has no rate plan (no_rate_plan), its price was set by hand (manual_price: edit that room to change it), none of its selected nights is still to come (no_nights), or its plan has no positive price for a selected night (missing_rate). A re-rated room keeps its plan, dates, occupancy, assignment and sold policy terms.

The operation runs in one transaction behind the property gate, so it is ordered with every other reservation and folio write of the property. Applying first checks every selection, which must carry the expected_total and the fingerprint (as expected_fingerprint) its preview showed. The fingerprint covers each selected room’s selected nights at their resulting prices and each skip decision, rooms ordered by id and nights by date, so rates that moved without changing the total (two nights swapping prices, one room gaining what another loses) are caught too. A reservation at another version, whose re-rated total differs from expected_total, or whose fingerprint differs from expected_fingerprint, makes the whole request answer 412 rerate.stale (although it takes no If-Match), which lists them in reservation_ids, and an unknown reservation or one of another property answers 404 reservation.not_found; any refusal saves nothing. Each reservation whose price changes is saved with a new version and its own reservation.rerated audit entry and event (room ids and the difference in minor units with its currency); for an in-house room the folio reverses the changed nights’ charges and posts them again at the new price. One reservation.bulk_rerated entry on the property records how many reservations, rooms and nights changed and the total difference. Reservations that do not change are not written. Amounts are exact minor units of the property’s currency on the wire as decimal strings; a difference may be negative.

max_sell is the most rooms a plan may sell on a night through every channel together: the front desk, other direct sources and the OTAs. It is set on base plans in the calendar like the other restrictions (a cell, a weekday-filtered range, paste, fill and Undo) and is in RateDay as max_sell (0–9999, null for no cap). A derived plan inherits its parent’s value, like every restriction, and counts its own sales against it.

  • Sold count. A plan’s sales on a night are the rooms of reservations that are not cancelled whose rate_plan_id is the plan, on nights before their release: the sale definition of the room-type ledger (inventoryDeltas), read from the stored reservations in the same transaction, so no second ledger can drift from them. An OTA room carries the local plan its Channex rate plan maps to, set when the booking is imported. An OTA room whose Channex plan is not mapped has no plan: it counts against no cap (RateWindow.sales.unattributed, and unattributed_nights in Channels health); once the operator maps that plan, the booking’s next imported revision attributes the room. The calendar shows each plan’s sold rooms against its cap and the unattributed OTA rooms per room type.
  • Quotes answer the reason max_sell for a plan that sold its cap on a night it would add. Nights a room already sold on the plan keep selling, as with stop sell.
  • Saving any reservation change checks the plan-nights it adds, all its rooms together, under the property’s write lock, so two concurrent bookings cannot both take the last room: beyond the cap it answers 409 rate_plan.max_sell_reached with max_sell_nights. A room’s max_sell_override_reason (1–1000 characters, permission reservations.override_sales_cap, built-in: administrators) sells it anyway; the audit entry reservation.max_sell_overridden records the room, the plan, the nights and the reason. The reason is not stored on the room. OTA bookings are facts and are never refused.
  • Publication. Channex receives the remaining count, max(0, cap − sold), in max_sell (Restrictions.RestrictionUpdate.max_sell; null removes the limit), on every write of the plan’s restrictions, and again whenever the plan’s sales change, even when the room type’s availability does not (another plan of the same nights). A plan with nothing left is also stop-sold, and its room type’s availability closes when no other plan sells. See ARI publication for why the remaining count, not the cap, is sent.

Channex publication consumes the outbox with conservative recovery. Fees/taxes and quote expiry/holds remain planned. A plan’s policy_id names its booking policy; the deprecated cancellation preset remains compatibility input. See ADR 0027.