Occupancy pricing through Channex to the channels
What a rate plan’s sale basis, occupancy curve, age bands and package become on the way from GuestCraft (the PMS) through Channex to each channel. GuestCraft writes only to Channex; how Channex forwards a value to a channel is configured in the Channex channel mapping, outside GuestCraft. Product rules are in ADR 0027 and ADR 0029; the publisher is described in ARI publication.
Verified means read on the cited Channex page or in the dated Channex OpenAPI snapshot
(packages/channex-simulator/src/openapi.snapshot.json, captured 2026-10-28). Unverified
means no documentation says it: it needs a staging run or the channel’s own confirmation, as
listed in the certification checklist. Simulator tests prove
GuestCraft’s side only, never a channel’s behaviour.
PMS → Channex
Section titled “PMS → Channex”| GuestCraft setting | What GuestCraft sends or requires in Channex | Status |
|---|---|---|
| Sale basis per room | A per_room Channex plan; one rate per night. A per-room plan has one occupancy option, “for maximum occupancy”. |
Verified: rate plans, snapshot RatePlans.RatePlan, Restrictions.RestrictionUpdate |
| Sale basis per person | A per_person Channex plan in manual rate mode with one option for each adult count from 1 to the plan’s maximum (“Occupancy Option for each possible count of adult guests”), at most 18. |
Verified: rate plans, size limits |
| Occupancy curve (per adult or explicit) | Not sent as a rule: GuestCraft computes each adult count’s absolute price every night and sends them all in rates: [{occupancy, rate}], “Alternative to rate for Rate Plans with multiple Occupancy Options”. A night with any count unpriced is sent as stop sell. |
Verified: ARI, snapshot Restrictions.OccupancyRate |
| Derived plan | Computed by GuestCraft and sent as absolute prices for every occupancy; the Channex plan must not inherit or derive (no derived_option rate rules, inheritance off). |
Verified that the fields exist: snapshot RatePlans.OccupancyOption; the requirement is GuestCraft’s |
| Infant and child age bands | Not sent. The property’s Channex hotel policy must give every age from 0 to 21 the category the bands give it (next row), and the Channex plan’s infant_fee and children_fee must equal the bands’ nightly fees (0 for a missing band), or the plan is not published. A plan without bands prices no child and leaves Channex’s fees and ages to the channels. Channex adds the fees “if guest is Infant [Children], based at Hotel Age Policy”. GuestCraft charges a band’s fee for each night of the stay: a declared local rule, not a verified equivalence with Channex. |
Fields verified: rate plans, snapshot RatePlans.RatePlan. Unverified: per night or per stay, an integer fee’s unit |
| Hotel policy ages | Read, never written: the property’s hotel_policy_id (Properties.Property, “UUID of the property’s hotel policy, or null”), then GET /hotel_policies/{id}: a guest is an infant up to infant_max_age (at most 10) and a child up to children_max_age (at most 21), each nullable. Read at plan discovery, at mapping and on every publication run, stored with the ARI settings (agePolicy). Not read yet, no policy, or a null age a band needs: a plan with bands is refused at save, at mapping and at publication. |
Verified: snapshot GET /hotel_policies/{id}, HotelPolicies.HotelPolicy, Properties.Property.hotel_policy_id; hotel policies. Unverified: whether a channel applies the policy’s ages to the fees |
| Package (included services) | Not distributed. Channex’s ARI has no package field (snapshot Restrictions.RestrictionUpdate); the price includes the services, and the folio splits them out on posting. |
Verified (absence of a field) |
| Channex reads back | One rate per plan and night (Restrictions.RestrictionValues); GuestCraft assumes the main option’s and compares only that occupancy. The other occupancies cannot be read, so the nightly drift check republishes them on a rotating seventh of the horizon: every night within seven checks. Health warns drift_partial_occupancies. |
Field verified; which occupancy it is: unverified |
| OTA booking with children | Channex sends occupancy.ages “with information about children ages”; GuestCraft stores them when there is one age from 0 to 17 per child, and never invents one. Channex’s separate infants count is not read. |
Verified: bookings |
Channex → channel
Section titled “Channex → channel”| Channel | Sale basis | Occupancy curve | Age bands (fees) | Package |
|---|---|---|---|---|
| Booking.com | Standard or occupancy-based pricing (OBP), chosen per connection; with OBP “you can map each occupancy of a room type”, one primary mapping sending the restrictions. Verified: Booking.com mapping, Booking.com API example | Each Channex occupancy price maps to a Booking.com occupancy when the hotel uses OBP. Unverified: what Standard pricing takes from a per-person plan | Unverified: how children_fee/infant_fee reach Booking.com |
Not distributed; a meal plan is a Booking.com rate setting. Unverified |
| Expedia | Standard (“one price per rate”) or OBP (“each rate carries a price per occupancy option”); for OBP one mapping per occupancy, exactly one primary. Verified: Expedia API example | As Booking.com: each occupancy price maps to an Expedia occupancy option on OBP. Verified for the mapping; delivery unverified | Unverified: the page names only max_children |
Not distributed. Unverified |
| Airbnb | One rate plan per listing: “you can send just 1 price only”; map the lowest occupancy. Verified: Airbnb mapping | Not distributed: Airbnb takes “Guests Included” and a single “Price Per Extra Guest”, set in the Channex mapping’s pricing settings, the same for every extra guest. A per-adult curve with one extra-adult amount can be mirrored there by hand; an explicit curve cannot. Verified: same page, fact check 1.3 | Unverified: the page says nothing about children, nor whether children count as extra guests | Not distributed. Unverified |
| Agoda | “Agoda mapping will always be multi occupancy”; without occupancy prices “you can map the same occupancy to all the different occupancies”. Verified: Agoda mapping | Each occupancy price maps to an Agoda occupancy. Delivery unverified | Unverified | Not distributed. Unverified |
| Hostelworld | Private rooms sell at the room type’s maximum occupancy (“If there is a Double Room you should send the 2 person price”); dorms at the one-person price. Verified: Hostelworld mapping | Only one occupancy is used: the maximum for a private room, 1 for a dorm bed. Map the per-person plan’s matching occupancy. Unverified: how Channex picks the occupancy of a per-person plan | Unverified | Not distributed. Unverified |
| Other channels | Depends on the channel’s Channex adapter; no Channex mapping guide documents occupancy for the rest. Unverified | Unverified | Unverified | Not distributed. Unverified |
What each channel can represent
Section titled “What each channel can represent”A plan declares where it is sold (distribution): direct only, never published to Channex, or
through Channex on a list of channel kinds. Channex’s public API lists no channel or channel
mapping of a property (no such operation in the OpenAPI document, SHA-256 50d36d7e…), so the
declaration is GuestCraft’s, and each kind is held to what its Channex mapping guide documents
(CHANNEL_CAPABILITIES in packages/core/src/reservations/channel-capabilities.ts):
| Channel kind | Occupancy it can price | Child and infant fees | Source |
|---|---|---|---|
booking_com |
Every adult count at its own price (occupancy-based pricing connection) | Not documented: refused | Booking.com mapping |
expedia |
Every adult count at its own price | Not documented: refused | Expedia API example |
agoda |
Every adult count at its own price (“always be multi occupancy”) | Not documented: refused | Agoda mapping |
airbnb |
One price up to the guests included, then one constant amount per extra guest: “you cant have different price for 1st extra or 2nd extra” | Not documented: refused | Airbnb mapping |
hostelworld |
One price, whoever stays | Not documented: refused | Hostelworld mapping |
| None declared | Conservative: what every channel with per-guest pricing represents, one constant amount per extra adult | Refused | This document |
A per-person curve is representable with one constant step when the base price applies up to
the base occupancy and each adult above it adds the same amount (a per-adult curve without a
fewer-adult discount, or explicit adjustments of 0 up to the base and equal steps after it). A
percentage-derived plan on such a channel is refused (derivation): rounding would break the
constant step. One assessment, distributionProblem, serves saving the plan
(rate_plan.not_distributable with field), mapping it (rate_plan.not_distributable with
the plan and field) and every publication run (the plan alone is stop-sold, health
plan_channels_mismatch with the field in planIssues, while the other plans keep publishing);
it covers every plan sharing the same pricing root and the property’s hotel policy ages. A plan sold direct only cannot be mapped.
What GuestCraft refuses at save time, and why
Section titled “What GuestCraft refuses at save time, and why”A plan is saved only if Channex can represent it exactly (rate_plan.not_distributable, or
rates.invalid for a value that is wrong for any channel):
- More than 18 adult counts (
occupancy/max): Channex sells at most 18 occupancies. - A maximum above the room type’s adults, or a base outside 1–maximum (
rates.invalid): the per-person plan sells every count from 1 to its maximum. - More than one infant band or one child band, or an infant band older than the child band
(
ageBands): Channex has exactly one infant fee and one child fee per plan. - A child age above 17 (
ageBands/<i>): the channels count guests from 18 as adults. - What the declared channels cannot represent (
occupancy/curve,derivation,distribution, orageBandsfor a child or infant fee above 0), as above. - Age bands the property’s Channex hotel policy does not share (
ageBands), on a property linked to Channex: the policy not read yet, no policy, or another category for any age from 0 to 21. A plan sold direct only, or without bands, is not compared. - A derived plan with its own curve or bands, or another sale basis than its root
(
rates.invalid): a derived plan prices every occupancy from its root’s (ADR 0027), so its Channex plan has the root’s options and fees. - What a published plan’s Channex plan holds (
rate_plan.channex_locked, onsellMode,occupancy/maxorageBands): Channex fixessell_modewhen a plan is created, and its occupancy options and fees would no longer match, which stops the plan’s publication; a different setting needs a new plan and a new mapping.
A night whose curve leaves an adult count without a positive price is not refused at save time: that night is unsellable and is published as stop sell for the whole plan. Packages are not refused either: they are never distributed, and the price published includes them.
At mapping time, and on every publication run, the Channex plan must match the local plan
(options, rate mode and, when the plan has bands, the hotel policy’s ages and the fees as above),
and the plan must be distributable on its declared channels; health names
plan_occupancy_mismatch, plan_age_policy_mismatch, plan_fees_mismatch or
plan_channels_mismatch until it is.