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:

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" } ] }

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:

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%"}}
  ]
}

What the wire supports today