Error dictionary
Every error answers with the same envelope — an HTTP status plus a stable
machine key in message:
{ "status": "error", "message": "errors.qc.unknown_room_type" }
Match on the key. Keys never change meaning; new ones may appear.
General
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.unauthorized |
401 | Missing, expired or malformed bearer token | Get a fresh token via POST /oauth/token; refresh proactively (tokens last 1 h) |
errors.not_found |
404 | The addressed resource does not exist or is outside your token's property scope | Check the id; check GET /properties for what your token can see |
errors.validation_error |
422 | Request body failed validation | The reference documents each field's type and bounds |
errors.qc.analytics_upstream_error |
502 | The analytics service (analytics.quendoo.com) is unreachable or returned a non-2xx status — QC forwards this as a gateway error rather than pretending a stale zero |
Retry with backoff; the response is idempotent, so repeated polls do not double-count anything. Persistent failure means the upstream is down — contact us |
Authentication
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.invalid_client |
401 | Unknown client_id, revoked client, or secret mismatch |
Verify credentials; contact us if the client was revoked |
errors.qc.invalid_scope |
422 | Requested scope is not in your client's allowed set | Request a subset of your granted scopes, or ask us to widen them |
errors.qc.insufficient_scope |
403 | Your token lacks the scope this endpoint needs | See scopes; request the right scope at token time |
errors.qc.rate_limit_exceeded |
429 | Budget exhausted for this endpoint bucket | Back off until X-RateLimit-Reset; batch your pushes (limits) |
errors.qc.idempotency_conflict |
409 | An Idempotency-Key was reused with a different body |
Use a fresh key per logical operation; replay only with the identical body |
Inventory writes
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.unknown_room_type |
422 | A room_type_id in values is not mapped on this property |
Re-read GET …/room_types; ids are per property |
errors.qc.unknown_room_or_rate |
422 | The (room_type_id, rate_plan_id) pair does not exist | Re-read GET …/rate_plans — a rate plan id is only valid with its own room_type_id |
errors.qc.batch_too_large |
422 | The request expands to more than 20 000 day-rows | Split the push into smaller date ranges |
errors.qc.rate_or_prices_required |
422 | A row carries both rate and prices, or neither |
Exactly one per row: rate (per_room shortcut) or prices (occupancy-code map) |
errors.qc.unknown_acm_code |
202 warning | A prices key is not in the pair's vocabulary |
Read the rate plan's occupancies; see pricing |
errors.qc.acm_not_priceable |
202 warning | The code is computed (derived / fee / cost_free), not stored |
Push only manual codes; derived values follow their source automatically |
errors.qc.prices_belong_to_parent |
202 warning | Price push on POST /inventory/rates addressed to a CHILD rate plan whose parent is mapped to QC |
Push to the parent_rate_plan_id named in the warning |
errors.qc.price_on_derived_rate |
202 warning (POST /inventory/rates) or 200 row-error (POST /rate_plans/{id}/rates) |
Price push on a derived (child) rate plan whose parent is NOT mapped to QC, or a POST /rate_plans/{id}/rates row that carries prices for a child. Restrictions on the same row still apply on POST /inventory/rates; on POST /rate_plans/{id}/rates the whole row is rejected (send restrictions in a separate row) |
Push prices to the parent legacy rate plan; leave the child alone — its prices resolve from the parent's chain at read time |
errors.qc.parent_rate_plan_not_found |
422 | POST /rate_plans with a parent_rate_plan_id that does not resolve to any QC-mapped rate plan |
Pass an existing external id (or omit for a non-derived plan) |
errors.qc.parent_rate_plan_not_on_property |
422 | POST /rate_plans with a parent_rate_plan_id that resolves to a plan on a DIFFERENT property |
Pick a parent that lives on the same property as the child |
errors.qc.derivation_requires_parent |
422 | POST /rate_plans with a derivation block but no parent_rate_plan_id |
Add parent_rate_plan_id, or drop derivation for a non-derived plan |
errors.qc.endpoint_not_allowed |
403 | Token scope OK, but its endpoint_scopes list is populated AND the current route's name is not in it (nor under a matching qc.<resource>.* wildcard) |
Widen the token's endpoint_scopes on the mint UI to include this route, or issue a new token with the desired allow-list |
errors.qc.prices_map_required |
202 warning | The rate scalar used on a pair that is not per_room |
Send a prices map keyed by occupancy code |
errors.qc.restriction_payload_empty |
422 | A restrictions row carries no restriction field at all | Send at least one of min_los, max_los, stop_sell, closed_to_arrival, closed_to_departure |
errors.qc.multi_property_batch_not_allowed |
422 | One batch tried to span more than one property | One request = one property; split per property |
Catalogue & structure
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.room_created_map_failed |
422 | The room was created but its QC mapping could not be established | Retry GET …/room_types — if the room is missing, contact us; do not re-POST blindly |
errors.qc.rate_plan_created_map_failed |
422 | Same as above, for a rate plan | Same as above |
errors.qc.partner_code_taken |
409 | POST /properties (or a subsequent PATCH) attempted to set a partner_code already claimed by another property on this account |
Pick a different code — codes are unique within a customer's group |
errors.qc.enrolment_failed_orphan_property |
500 | POST /properties created the legacy row but the QC map row failed to insert (rare — retry-safe partial write) |
Retry the same request; the create is idempotent under Idempotency-Key and the second call will reuse the orphan property. If the second call also fails, contact us with the request id |
errors.qc.occupancies_require_single_room |
422 | POST /rate_plans with an explicit occupancies block AND more than one room_type_ids[] — different bed capacities mean different legal occupancy codes, so a shared list would be ambiguous |
Split into one request per room, OR omit occupancies and let the vocabulary auto-derive from each room's bed capacity |
errors.qc.occupancy_pricing_not_supported |
422 | POST /inventory/rates sent per-occupancy prices to a rate plan whose pair is not per_person / per_occupancy — per_room pairs only accept a single scalar rate |
Send rate for a per_room pair, or prices for a per-occupancy pair |
Configuration surfaces (v2)
Every write on the v2 configuration entities (extras, promotions,
policies, gallery, ...) returns the same envelope. Validation
failures come back as errors.validation_error (422) with a
per-field breakdown under errors[<field_path>][] — the keys below
are the ones the API itself raises, not the per-field customer.*
translation keys.
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.payment_policy_in_use |
409 | DELETE on a payment policy still referenced by at least one rate plan | Detach the policy from every rate plan first, or replace with another before deleting |
errors.qc.cancellation_policy_in_use |
409 | Same, for cancellation policies | Same |
errors.qc.property_id_not_granted_to_client |
422 | An auto-email template's property_ids[] contained an id outside the OAuth client's granted property set |
Only include ids returned by GET /properties |
errors.qc.gallery_image_in_use |
409 | DELETE on a gallery image with active usages[] |
Re-post with ?force=1 to sever usages and delete anyway, or unhook the image from the referencing entity first. The 409 body carries the usages list so you know what points at the image |
errors.qc.gallery_upload_failed |
500 | The upload succeeded at the API layer but the storage adapter refused | Retry; if it persists, contact us with the request id |
errors.qc.sfp_min_gt_max_nights |
422 | Special-offer sfp.min_nights > sfp.max_nights |
Order the pair correctly |
errors.qc.age_group_range_inverted |
422 | A child age-group's from_age is greater than its to_age on PUT /age-groups |
Order the range so from_age ≤ to_age |
Per-field validation keys (returned inside errors[<field>][], not
in message) follow the pattern customer.<resource>.messages.<rule>
and are meant for direct display via the FE i18n dictionary; the API
consumer usually only needs the HTTP 422 signal.
Bookings
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.booking_create_failed |
422 | POST /bookings reached the write path but the underlying CreateBooking use case refused it (usually a rate/availability mismatch caught at the last step) |
The response's errors map names the offending field. Fix the payload and re-submit under the same Idempotency-Key — the failure is not cached, so the retry re-runs the write cleanly |
Webhooks & subscriptions
| Key | Status | Cause | Fix |
|---|---|---|---|
errors.qc.subscription.unknown_property |
422 | Subscription referenced a property outside your scope | Use ids from GET /properties |
errors.qc.deliveries.unknown_status |
422 | Delivery-log filter with an unknown status value |
Allowed: pending, delivering, delivered, failed, dead |
errors.qc.webhook.missing_channel / .unknown_channel |
400 | Inbound webhook URL without a valid channel code | Channel-partner integrations only — check your assigned channel code |
errors.qc.webhook.missing_property / .payload_invalid |
422 | Inbound webhook payload missing external_property_id / malformed |
Compare against the inbound webhook schema in the reference |
Inventory writes — per-row error codes
POST /rate_plans/{id}/rates returns per-row errors in the response
errors[] array (not in message) so a partial batch success is
possible. The row's HTTP status is always the batch envelope's
status; look at each errors[i].code to decide what to fix.
code |
Cause | Fix |
|---|---|---|
malformed_row |
The batch entry is not a JSON object. | Send {date, prices, restrictions} per row. |
bad_date |
date missing or not YYYY-MM-DD. |
Fix the date. |
bad_acm_key |
An occupancy key in prices is empty / not a string. |
Use non-empty string codes like A1, ROOM. |
unknown_acm_code |
The code is not part of this (rate_plan, room_type) pair's vocabulary. The error row carries unknown_code (the offending code) AND allowed_codes (the pair's full legal set — same list you'd see under GET /rate_plans/{id}.occupancies[]) so you can self-correct without a second lookup. Before this check landed, unknown codes were silently dropped and the row still reported success — pushing per-person codes to a per_room pair would 200 with accepted:1, errors:[] and the values would vanish. |
Push only codes from allowed_codes. ROOM on per_room pairs; A1/A2/AEB/CRB/… on per_person / per_occupancy. |
bad_restriction_key |
A restriction key is not in the whitelist. | See concepts-inventory. |
write_failed |
The underlying repo threw. message carries the exception detail. |
Retry once; if it recurs, open a support ticket with the response's X-Qc-Request-Id. |
PMS v1 facade — byte-strict error strings
Legacy PMS v1 responses do not carry stable errors.* keys —
their message field is a plain-English sentence, sometimes with
the offending payload appended after a for: prefix. Match on the
exact string. The PMS v1 facade
promises byte-parity, so every string in the table below is
guaranteed to survive the QC port unchanged.
<dump> is a PHP print_r of the row that failed. Legacy leaks
it into the message on purpose; log-scrapers that anchor on the
literal prefix still match.
Availability/updateAvailability — reference
| Status | Legacy message |
|---|---|
| 400 | Bad request data! Wrong format of the post structure (missing 'values' structure - check documentation)! |
| 400 | Invalid qty (qty can't be < 0) for: <dump> |
| 400 | Missing parameters qty and/or is_opened for: <dump> |
| 400 | Both parameters 'room_id', 'ext_room_id' can't be set at the same time!<dump> |
| 400 | Missing room_id for: <dump> |
| 400 | Not found property room_id for: <dump> |
| 400 | Invalid date format (must be 'YYYY-MM-DD') for: <dump> |
| 400 | Invalid date_from/date_to format (must be 'YYYY-MM-DD') for: <dump> |
| 400 | Missing date or date_from/date_to for: <dump> |
| 400 | Invalid params for: <dump> |
| 404 | Can't find room id for the given ext room id for: <dump> |
Availability/getAvailability — reference
| Status | Legacy message |
|---|---|
| 400 | Missing/Invalid required request data! |
| 400 | Both parameters 'room_id', 'ext_room_id' can't be set at the same time! |
| 404 | Can't find room id for the given ext room id! |
Booking/getBookings — reference
| Status | Legacy message |
|---|---|
| 400 | Invalid value for the 'type' parameter! |
Booking/ackBooking — reference
| Status | Legacy message |
|---|---|
| 400 | Bad request data! |
| 400 | The ack was already sent! |
| 400 | Missing booking_items[].booking_item_id and/or booking_items[].ext_reservation_id! |
| 404 | Missing booking revision! |
| 404 | Missing booking! |
| 404 | Missing booking item! |
Booking/postRoomAssignment — reference
| Status | Legacy message |
|---|---|
| 400 | Bad request data! |
| 400 | Error when try to post room assignment! |
| 404 | Missing booking! |
| 404 | Missing booking item! |
Property/postExternalPropertyData — reference
| Status | Legacy message |
|---|---|
| 400 | Bad post request data! |
Property/getRoomsDetails — reference
| Status | Legacy message |
|---|---|
| 404 | underlying exception message — legacy propagates the exception's getMessage() verbatim; not enumerable |
Property/getBookingOffers — reference
| Status | Legacy message |
|---|---|
| 400 | Bad request data! |
| 400 | Bad request data for the 'guests'->'adults' param! |
| 400 | Bad request data for the 'guests' param! |
| 400 | Bad request data for the 'guests'->'children_by_ages'->'age' param! |
| 404 | Booking module '{bm_code}' not found or inactive. |
| 404 | Property owner not found. |
General v1 wrapper errors
Every PMS v1 method routes through the same authentication and top-level exception envelope. These override the method-specific tables above.
| Status | Legacy message |
|---|---|
| 401 | Wrong api key! * (missing) / Wrong api key (unknown) / Wrong api key! ** (inactive) |
| 404 | Missing endpoint class: {epClass} |
| 404 | Missing endpoint method: {epMethod} |
| 404 | Any locally-caught \Throwable → 404 with Throw Error Exception:\n\tMsg:…\n\tCode:…. Legacy answered 404 to everything unhandled and QC preserves that shape (with the file/line leak stripped). |