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).