Pricing β the three sell types and occupancy codes
The single most important page in these docs. Read it before you design your
rates sync: it explains what you see in GET β¦/rates responses and which
prices can be written at all.
The sell type lives on the pair
Every room type Γ rate plan pair carries one of three pricing modes. The
sell_type field on a rate plan entry tells you which:
sell_type |
Meaning | Priced codes |
|---|---|---|
per_room |
One price per date for the whole room, regardless of who sleeps in it | the single code ROOM |
per_person |
Price depends on the accommodation: tariffs for n adults, plus codes for extra beds and children | A1β¦An, AEB, TRB/TEB/TNB, CRB/CEB/CNB, IRB/IEB/INB |
per_occupancy |
Price depends on the position: each additional person has their own code | A1β¦An, Apn1β¦, Tpn1β¦, Cpn1β¦, Ipn1β¦ |
Reading an occupancy code
The vocabulary is combinatorial β person group Γ bed group:
| Person | Bed | ||
|---|---|---|---|
A |
adult | RB |
regular bed |
T |
teenager | EB |
extra bed |
C |
child | NB |
no bed (shares) |
I |
infant |
So CRB is a child on a regular bed, AEB is an adult on an extra bed,
INB is an infant without an own bed. Child-family codes carry an age range
defined by the property.
Two special families:
A1,A2,A3β¦ β the price of the whole tier of n adults on regular beds, not a per-person price. A room sold atA2 = β¬220costs β¬220 for the two adults together.ROOMβ the single synthetic code of aper_roompair.Apn{i}/Cpn{i}β¦ (per_occupancyonly) β "person number": the i-th extra adult / child as an individual add-on position.
Worked example
Hotel Panorama's Double Room Γ Standard B&B pair is per_person:
| Code | Configured as | Nightly value |
|---|---|---|
A2 |
manual β this is where the price is written | β¬220.00 |
A1 |
derived: A2 β 10 % | β¬198.00 (computed) |
AEB |
manual | β¬45.00 |
CRB (3β11.99 y) |
derived: A2 β 75 % | β¬55.00 (computed) |
IRB (0β2.99 y) |
cost-free | β¬0.00 |
A quote for 2 adults + 1 child on a regular bed is A2 + CRB = β¬275.00/night.
Manual vs derived β which codes accept prices
Each code on a pair is configured by the hotelier as one of:
| Kind | Price comes from | Writable? |
|---|---|---|
manual |
the calendar β a stored nightly value | yes |
derived |
another code Β± an amount or percentage, computed at read time | no |
fee |
a fixed constant | no |
cost_free |
always 0 | no |
inactive |
not sellable | no |
Only manual codes hold calendar rows. Everything else is computed when the price is read, so a "price" pushed for a derived code has nothing to land on β the API refuses it rather than silently ignoring it.
Parent and child plans β prices go to the parent
A child rate plan (one with parent_rate_plan_id set) has no prices of its
own: every nightly value is computed from the parent's calendar with a
percentage the hotelier maintains per check-in period. Push prices to the
parent; a price push addressed to a child plan is refused. Restrictions are
the opposite β the child owns its own, and pushing them to a child is normal.
The wire describes all of this β read occupancies
Every rate-plan entry self-describes its pricing. You never have to guess which codes exist or which accept prices:
{ "id": "a5d91c3e-6b2f-4d8a-9c4e-1f7b3a8d5c26",
"sell_type": "per_person",
"parent_rate_plan_id": null,
"prices_writable": true,
"occupancies": [
{ "code": "A2", "kind": "manual", "persons": 2 },
{ "code": "A1", "kind": "derived", "persons": 1,
"derived_from": "A2", "modifier": ["decrease_by_percent", 10] },
{ "code": "AEB", "kind": "manual" },
{ "code": "CRB", "kind": "derived",
"derived_from": "A2", "modifier": ["decrease_by_percent", 75] },
{ "code": "IRB", "kind": "cost_free" } ] }
prices_writable: falsemarks a child plan β push prices to itsparent_rate_plan_idinstead. A child still showsoccupancies(the parent's vocabulary β it is what quotes the child's stays).modifieris a[verb, value]pair:increase_by_amount,decrease_by_amount,increase_by_percent,decrease_by_percent. Derivations can chain (A1 β A2 β A3) βderived_fromis always the direct source.- Dated offer combinations the hotelier configures on adult tiers (e.g. "2 adults + 1 child" priced as an offset for a season) are handled inside Quendoo and are deliberately not part of the wire.
Setting a pricing model β one-shot decision tree
Which endpoint to reach for depends on how much control you need
over the occupancy vocabulary β the A1/A2/AEB/CRB/β¦ list a
per_person / per_occupancy pair exposes.
| Your case | Call | What you get |
|---|---|---|
| Simple pair, any sell type, one or many rooms β you're happy with the standard vocabulary the room's bed capacity implies | POST /rate_plans without occupancies |
Auto-derive per room. Heterogeneous multi-room OK. |
Single room, you want to override the vocabulary at creation (declare manual / derived codes up front) |
POST /rate_plans with occupancies[] |
Explicit vocabulary land, sanitised against the room. Multi-room + occupancies returns 422. |
Existing rate plan β flip sell_type without changing anything else |
PATCH /rate_plans/{id} with just sell_type |
Legacy rooms_rates propagates; vocabulary re-derives per room; other fields untouched. |
| Existing rate plan β reset the whole pricing model (sell type + explicit vocabulary) declaratively | PUT /rate_plans/{id}/pricing-model |
Idempotent β same body twice = same state; per-row optimistic-lock; response echoes pairs_updated. |
Everything after these calls speaks the same shape:
GET .../rate_plans/{id} returns occupancies[] for every pair;
POST .../rate_plans/{id}/rates validates prices against that
vocabulary; a rejection echoes the full allowed_codes list so
you can self-correct without a second call.
Per-person pairs are set at rate-plan create time
The A1/A2/AEB/CRB/β¦ vocabulary is not a fixed alphabet β it is
computed from each room's bed capacity (acm_settings: how
many regular beds, extra beds, no-bed slots the room has, and
which person codes each accepts). Two different rooms with
different bed layouts on the same per_person rate plan expose
different occupancy vocabularies.
POST /rate_plans accepts sell_type as one of per_room,
per_person, per_occupancy. When it is per_person /
per_occupancy, the server auto-derives the vocabulary per
room β the partner does not hand-list the codes, and
heterogeneous multi-room requests get the right per-room vocab
per row. A single declarative call stands up a real
per_person pair.
POST /v1/qc/properties/{ext}/rate_plans
{
"title": "Standard B&B",
"room_type_ids": ["<uuid-standard-queen>", "<uuid-deluxe-king>"],
"sell_type": "per_person",
"meal_plan": "BB"
}
After this call GET .../rate_plans/{id} carries the real
occupancies[] per pair; POST .../rate_plans/{id}/rates
validates prices against those codes; an unknown_acm_code
rejection echoes the full allowed_codes list so a partner can
self-correct in one round-trip.
The dashboard bed-layout step is still available when a hotelier wants to customise a room's bed layout after onboarding β setting a custom layout re-derives the vocabulary on every subsequent rate-plan create. Two things a partner cannot do today through the API alone:
- Set / override a room's bed layout β the room inherits the hotelier's account default on create; layout changes stay a dashboard concern.
- (Was parked; now shipped) Override the auto-derived vocabulary
on an existing pair β that's what
PUT /rate_plans/{id}/pricing-modelbelow is for.
Optional explicit occupancies on create
For single-room custom cases β the plan carries one
room_type_ids[] entry and the caller wants explicit derived
codes β POST /rate_plans accepts an optional occupancies[]:
POST /v1/qc/properties/{ext}/rate_plans
{
"title": "Standard B&B",
"room_type_ids": ["<uuid-standard-queen>"],
"sell_type": "per_person",
"occupancies": [
{"code": "A2"},
{"code": "A1", "pricing": {"derived_from": "A2", "adjust": "-10%"}},
{"code": "AEB"}
]
}
pricing.adjust is [sign][number]% for a percentage or a bare
[sign][number] for a flat amount; missing pricing block means
manual. Codes not valid for the room's bed capacity are dropped
silently by the same sanitizeSettings pass the update path uses.
Multi-room + occupancies is refused with
errors.qc.occupancies_require_single_room β a single hand-
written list can't fit heterogeneous rooms. Omit occupancies
for multi-room and let auto-derive per room take over.
Declarative escape hatch β PUT pricing-model
For the ~10% of cases where auto-derive doesn't cover the pair β
custom derivations, a partner rebuilding the pair's vocabulary
after the fact β PUT /rate_plans/{id}/pricing-model is
idempotent + declarative:
PUT /v1/qc/properties/{ext}/rate_plans/{id}/pricing-model
{
"sell_type": "per_person",
"occupancies": [
{"code": "A2"},
{"code": "A1", "pricing": {"derived_from": "A2", "adjust": "-10%"}}
]
}
sell_typeis required. Same alphabet as POST.occupanciesoptional. When present, replaces the pair's vocabulary (single-room only; multi-room +occupanciesis a 422). Omitted β auto-derive per room.- Every non-deleted
rooms_ratesrow for the rate plan is updated with the wire values; per-row optimistic-lock via_version. Derivative rate plans (has_parent_rate = 1) carry no rooms_rates rows of their own and are skipped β the parent's update propagates by construction. - Idempotent: same body twice = same state.
- Response:
{external_rate_plan_id, sell_type, pairs_updated, explicit_occupancies}.
What the wire supports today
GET β¦/rate_plansβsell_type,parent_rate_plan_id,prices_writableand theoccupanciesvocabulary for every pair.GET β¦/rate_plans/{id}/ratesreturns the resolved per-day, per-code prices β manual and computed alike, exactly what a guest would be quoted.GET β¦/rate_plans/{id}/rates?consolidated=1returns the same data as period pricing β consecutive dates carrying identicalpricesmaps collapse into one{from, to, prices}row per pair. A partner pulling a full year (up to 730 days per call β the per-day read is capped at 90; consolidated lifts because the wire volume is bounded by distinct period count, not day count) gets a handful of periods per pair instead of 365 daily rows. Equality is normalised: key order and int-vs-float representation of the same numeric value do NOT split adjacent periods. Restrictions are NOT part of the equality key in this pass β hit the per-day read alongside if you need to reconcile restrictions.POST β¦/inventory/ratesaccepts prices for every sell type: theratescalar shortcut for per_room pairs, or apricesmap keyed by occupancy code β{"A2": 220.00, "AEB": 45.00}β validated against theoccupanciesvocabulary above. Onlymanualcodes accept values; a refused code comes back as a row-level warning (the batch never aborts), and a child plan answerserrors.qc.prices_belong_to_parentwith the id to push to instead. Writing a manual code also seeds calendar rows for its derived codes, so they become sellable the moment their source is priced.