The domain model
Five nouns carry the whole API. Everything else hangs off them.
Property (external_property_id)
├─ Room type "Double Room", "Family Apartment"
│ └─ × Rate plan → a PAIR — where the pricing mode lives
├─ Rate plan "Standard B&B" (parent) ← "Non-refundable" (child)
└─ Calendars
├─ availability per room type × date (rate-independent)
└─ prices + restrictions
per room type × rate plan × occupancy code × date
Property
The hotelier's property, addressed everywhere by its opaque
external_property_id (a UUID QC assigns). You never see Quendoo's internal
ids. GET /properties lists the properties your token is scoped to — it is
the discovery call and needs no scope.
Properties can be created by the partner through POST /properties, or
by the hotelier from the Quendoo dashboard. Either path lands the same
underlying legacy row plus a QC mapping; the API answer is the same. Fetch
the list of valid type_id values first via GET /property-types — it's
the reference-data discovery endpoint the create call needs.
Each property has a QC mode — legacy, shadow, live or disabled —
that gates whether your inventory pushes actually write. Partner-created
properties default to shadow (safer than legacy for a fresh channel);
ops flips to live when the mapping is validated. Either the hotelier or
the integrator can PATCH the mode. See
inventory writes.
Every property also carries an optional partner_code — a slot for
the partner's own identifier so they can address the property by their
code instead of our UUID (see the bidirectional-id contract in the
guide). Group-scoped uniqueness — a real code cannot repeat within one
owner's group. Null is the default; either side can PATCH it later.
Room type
A bookable unit category with a count (qty) and bed capacities
(adults / children / infants). Addressed by external_room_type_id.
Rate plan — and the pair
A sellable tariff ("Standard Bed & Breakfast", "Non-refundable"). A rate plan
is sold per room type: the combination of one room type and one rate plan
is a pair, and the pair is where the pricing mode
(sell type) is chosen. Because of that, the API
returns rate plans flattened across rooms: a plan attached to three room
types appears as three entries, each with its own room_type_id and its own
external_rate_plan_id.
Parent and child rate plans
A rate plan can derive from a parent (parent_rate_plan_id, exactly one level
deep — a child cannot have children):
- the child inherits the pair configuration (sell type, occupancy codes) wholesale;
- prices live only on the parent's calendar — the child's prices are computed from the parent (a percentage per check-in period, maintained by the hotelier);
- the child owns everything else: its own restrictions, open/close state, meal plan, payment and cancellation policies.
Practical consequence: push prices to the parent, push restrictions to whichever plan they belong to. See pricing.
Calendars
Two, with different granularity — this asymmetry is load-bearing:
| Calendar | Granularity | Holds |
|---|---|---|
| Availability | room type × date | open state, physical qty, offered qty, booked qty |
| Prices & restrictions | room type × rate plan × occupancy code × date | nightly price, min/max LOS, stop-sell, CTA/CTD, … |
Availability is rate-independent: one number of sellable rooms per room type per date, shared by all its rate plans. Prices and restrictions are per pair, per occupancy code.
Your own catalogue and the mapping
QC also stores your side of the world: PUT /catalogue declares the rooms,
rates, services, meals and beds your PMS has, by your own codes. Mapping your
codes onto Quendoo entities is then a PATCH with partner_code on the room
type or rate plan. The integration guide walks both steps.
Bidirectional-ids
Every URL segment that identifies a QC entity accepts EITHER the
canonical external UUID QC minted OR the partner's own
partner_code for that entity. This works today for the property,
room-type and rate-plan levels — the three anchors your PMS
touches most.
Practical shape:
GET /v1/qc/properties/{ext_property_id | partner_code}
GET /v1/qc/properties/.../room_types/{ext_room_type_id | partner_code}
GET /v1/qc/properties/.../rate_plans/{ext_rate_plan_id | partner_code}
Resolution order: exact match on the canonical UUID first (fast
index hit), fallback to partner_code on miss (also unique per
scope). Empty / null partner_code never matches — a room type
without a partner code cannot be picked up by an empty URL segment.
Practical upside: once you push partner_code at onboarding your
PMS can use its own catalogue ids everywhere in the URL —
GET /properties/PMS-HOTEL-4211/room_types/PMS-STANDARD-QUEEN.
The response still carries our UUIDs; store them if you care
about a stable id across a hotelier renaming their side.
Mode lifecycle
Every property carries a mode field on its map that gates the
inventory write path.
| Mode | Writes | When to be here |
|---|---|---|
legacy |
ignored | Property is still owned by the old channel manager; QC pushes short-circuit into dry. Safe placeholder while onboarding a partner alongside the incumbent. |
shadow |
validated + accounted, NOT applied | Every write full-validates, response says "dry": true. The parity harness compares what we WOULD have written to what legacy wrote. Diff score must be zero before flip to live. |
live |
applied | Real writes hit the calendar; hotelier and guests see the effect immediately. |
disabled |
rejected | Emergency stop. Property temporarily out of scope for QC — the mapping row stays, writes error, hotelier can still see the property in the dashboard. |
Transitions are one-way in practice: legacy → shadow → live.
disabled can freeze from any state. live → shadow is possible
but rare (post-incident audit); asks a PATCH with an explanation
that lands in the property audit log.
Occupancy taxonomy — default vs Qc-Rich-Occupancy
GET /properties/{ext}/room_types/{ext} returns occupancy as a
simple {adults, children, infants} triple by default — the
Booking-style taxonomy every OTA understands.
Partners who need the Quendoo-native taxonomy (multiple child age
bands, teen slot, extra-bed vs regular-bed distinction) send the
Qc-Rich-Occupancy: true request header on the READ, and get back
a richer occupancy object naming every group Quendoo tracks.
Writes always accept the simple triple; the rich form is a read convenience for PMSs that model bed layouts in detail.
See Pricing & occupancy codes for how those groups map to price rows.