> For the complete documentation index, see [llms.txt](https://mobile-calendar.gitbook.io/v1/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mobile-calendar.gitbook.io/v1/rest-api/channels.md).

# Channels

OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing).

## Get Booking.com rate plans (live from the portal) with the units they are sold on

> Reads the rate plans of the connected Booking.com hotel(s) LIVE from the portal — id, name, cancellation policy, meal plans, derived-plan relation and the units (Booking rooms) the plan is attached to, with the mobile-calendar rooms mapped to each unit. \`ratePlanId\` and \`unitId\` are exactly the ids used by GET/PUT /channels/booking/pricing (prices and per-date restrictions live there — this resource manages the plans themselves). Optional \`hotelId\` filter narrows to one hotel. Booking.com must be connected (otherwise 409). Required scope: \`channels.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"BookingRatePlansResponse":{"required":["data"],"properties":{"data":{"required":["ratePlans"],"properties":{"ratePlans":{"type":"array","items":{"$ref":"#/components/schemas/BookingRatePlanEntry"}}},"type":"object"},"meta":{"properties":{"total":{"type":"integer"},"hotels":{"type":"array","items":{"type":"integer"}},"ruids":{"description":"Booking.com request ids (for support tickets).","type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"},"BookingRatePlanEntry":{"description":"One Booking.com rate plan read LIVE from the portal, with the units (rooms) it is sold on.","required":["ratePlanId","name","hotelId","hotelName","policyId","policyName","mealPlanCodes","mealPlans","derived","units"],"properties":{"ratePlanId":{"description":"Booking.com rate plan id — the same id as `ratePlanId` in GET/PUT /channels/booking/pricing.","type":"integer"},"name":{"type":"string"},"hotelId":{"type":"integer"},"hotelName":{"type":"string"},"policyId":{"description":"Cancellation policy id (GET /channels/booking/policies).","type":"integer","nullable":true},"policyName":{"type":"string","nullable":true},"mealPlanCodes":{"description":"Booking.com meal plan codes (19 = Breakfast; see POST request schema for the full list).","type":"array","items":{"type":"integer"}},"mealPlans":{"type":"array","items":{"type":"string"}},"derived":{"oneOf":[{"$ref":"#/components/schemas/BookingRatePlanDerived"}],"nullable":true},"units":{"type":"array","items":{"$ref":"#/components/schemas/BookingRatePlanUnit"}}},"type":"object"},"BookingRatePlanDerived":{"description":"Derived (child) rate plan configuration: prices follow the parent plan with a percentage change. null = standalone plan.","required":["parentRatePlanId","priceChangePercent"],"properties":{"parentRatePlanId":{"description":"Parent Booking.com rate plan id (must exist on the same hotel).","type":"integer"},"priceChangePercent":{"description":"-10 = 10% cheaper than the parent, 20 = 20% more expensive (Booking.com Percentage 1-200).","type":"integer","maximum":100,"minimum":-99},"followsPrice":{"description":"Child follows parent price changes (default true).","type":"boolean"},"followsRestrictions":{"description":"Child follows parent restrictions (default false).","type":"boolean"},"followsPolicyGroup":{"description":"Child follows parent cancellation policy (default false).","type":"boolean"},"followsClosed":{"description":"Booking.com FollowsClosed mode (0 = independent).","type":"integer","maximum":3,"minimum":0}},"type":"object"},"BookingRatePlanUnit":{"required":["unitId","unitName","mappedRoomIds"],"properties":{"unitId":{"description":"Booking.com room (unit) id — the same id as `unitId` in GET /channels and GET/PUT /channels/booking/pricing.","type":"string"},"unitName":{"type":"string"},"mappedRoomIds":{"description":"mobile-calendar rooms mapped to this unit (GET /rooms); empty = unit not mapped in the channel manager.","type":"array","items":{"type":"integer"}}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem409":{"description":"Conflict with resource state or integrity constraints.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"409 Conflict","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"}}},"paths":{"/channels/booking/rate-plans":{"get":{"tags":["channels"],"summary":"Get Booking.com rate plans (live from the portal) with the units they are sold on","description":"Reads the rate plans of the connected Booking.com hotel(s) LIVE from the portal — id, name, cancellation policy, meal plans, derived-plan relation and the units (Booking rooms) the plan is attached to, with the mobile-calendar rooms mapped to each unit. `ratePlanId` and `unitId` are exactly the ids used by GET/PUT /channels/booking/pricing (prices and per-date restrictions live there — this resource manages the plans themselves). Optional `hotelId` filter narrows to one hotel. Booking.com must be connected (otherwise 409). Required scope: `channels.read`.","operationId":"getBookingRatePlans","parameters":[{"name":"hotelId","in":"query","description":"Booking.com hotel id (GET /channel-manager); omitted = all connected hotels.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Rate plans","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRatePlansResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Hotel does not belong to this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"409":{"description":"Booking.com is not connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem409"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"Booking.com did not return the data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Create a Booking.com rate plan and attach it to units

> Creates a new rate plan on Booking.com (name + optional derived relation) and attaches it to the listed units with a cancellation policy, meal plans and optional booking restrictions — the same two-step operation as the panel's Booking rate-plan dialog. Everything is validated against LIVE portal data before any write: units must exist on the hotel, \`policyId\` must be one of GET /channels/booking/policies, \`derived.parentRatePlanId\` must be an existing plan of the same hotel. \`hotelId\` is required only for multi-hotel accounts. After success the local mirror used by GET /channels is refreshed, so the new \`ratePlanId\` can be used immediately in PUT /channels/booking/pricing to send prices. If Booking.com creates the plan but rejects attaching units, the response is 502 and names the created \`ratePlanId\` — finish with PATCH instead of retrying POST (which would create a duplicate). Required scope: \`channels.update\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"BookingRatePlanWriteRequest":{"description":"Creates a rate plan and attaches it to the listed units. For partial updates see BookingRatePlanUpdateRequest (PATCH).","required":["name","unitIds","policyId"],"properties":{"name":{"description":"Rate plan name shown on Booking.com.","type":"string","maxLength":255},"hotelId":{"description":"Required only when the account has more than one Booking.com hotel.","type":"integer","nullable":true},"unitIds":{"description":"Booking.com unit (room) ids to sell this plan on — from GET /channels or GET /channel-manager.","type":"array","items":{"type":"string"},"maxItems":50,"minItems":1},"policyId":{"description":"Cancellation policy id (GET /channels/booking/policies).","type":"integer"},"mealPlanCodes":{"description":"Booking.com meal plan codes: 0 None, 19 Breakfast, 21 Lunch, 22 Dinner, 10 Full board, 12 Half board, 1 All inclusive, … Empty list = no meals.","type":"array","items":{"type":"integer"}},"derived":{"oneOf":[{"$ref":"#/components/schemas/BookingRatePlanDerived"}],"nullable":true,"description":"Derived-plan config; null (or omitted on POST) = standalone plan. On PATCH: omitted = keep current, null = detach from parent."},"minAdvanceReservation":{"description":"Minimum advance before check-in to book (P1D = 1 day, P12H = 12 hours).","type":"string","pattern":"^P[0-9]{1,3}(Y|M|D|H)$","nullable":true},"maxAdvanceReservation":{"description":"Maximum advance before check-in to book.","type":"string","pattern":"^P[0-9]{1,3}(Y|M|D|H)$","nullable":true},"activeWeekdays":{"description":"Check-in weekdays the plan is bookable on; omitted = all days.","type":"array","items":{"type":"string","enum":["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]},"nullable":true}},"type":"object"},"BookingRatePlanDerived":{"description":"Derived (child) rate plan configuration: prices follow the parent plan with a percentage change. null = standalone plan.","required":["parentRatePlanId","priceChangePercent"],"properties":{"parentRatePlanId":{"description":"Parent Booking.com rate plan id (must exist on the same hotel).","type":"integer"},"priceChangePercent":{"description":"-10 = 10% cheaper than the parent, 20 = 20% more expensive (Booking.com Percentage 1-200).","type":"integer","maximum":100,"minimum":-99},"followsPrice":{"description":"Child follows parent price changes (default true).","type":"boolean"},"followsRestrictions":{"description":"Child follows parent restrictions (default false).","type":"boolean"},"followsPolicyGroup":{"description":"Child follows parent cancellation policy (default false).","type":"boolean"},"followsClosed":{"description":"Booking.com FollowsClosed mode (0 = independent).","type":"integer","maximum":3,"minimum":0}},"type":"object"},"BookingRatePlanResponse":{"required":["data"],"properties":{"data":{"required":["ratePlan"],"properties":{"ratePlan":{"$ref":"#/components/schemas/BookingRatePlanEntry"}},"type":"object"},"meta":{"properties":{"ruids":{"type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"},"BookingRatePlanEntry":{"description":"One Booking.com rate plan read LIVE from the portal, with the units (rooms) it is sold on.","required":["ratePlanId","name","hotelId","hotelName","policyId","policyName","mealPlanCodes","mealPlans","derived","units"],"properties":{"ratePlanId":{"description":"Booking.com rate plan id — the same id as `ratePlanId` in GET/PUT /channels/booking/pricing.","type":"integer"},"name":{"type":"string"},"hotelId":{"type":"integer"},"hotelName":{"type":"string"},"policyId":{"description":"Cancellation policy id (GET /channels/booking/policies).","type":"integer","nullable":true},"policyName":{"type":"string","nullable":true},"mealPlanCodes":{"description":"Booking.com meal plan codes (19 = Breakfast; see POST request schema for the full list).","type":"array","items":{"type":"integer"}},"mealPlans":{"type":"array","items":{"type":"string"}},"derived":{"oneOf":[{"$ref":"#/components/schemas/BookingRatePlanDerived"}],"nullable":true},"units":{"type":"array","items":{"$ref":"#/components/schemas/BookingRatePlanUnit"}}},"type":"object"},"BookingRatePlanUnit":{"required":["unitId","unitName","mappedRoomIds"],"properties":{"unitId":{"description":"Booking.com room (unit) id — the same id as `unitId` in GET /channels and GET/PUT /channels/booking/pricing.","type":"string"},"unitName":{"type":"string"},"mappedRoomIds":{"description":"mobile-calendar rooms mapped to this unit (GET /rooms); empty = unit not mapped in the channel manager.","type":"array","items":{"type":"integer"}}},"type":"object"},"ApiProblem400":{"description":"Request syntax/format error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"400 Bad Request","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem409":{"description":"Conflict with resource state or integrity constraints.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"409 Conflict","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"}}},"paths":{"/channels/booking/rate-plans":{"post":{"tags":["channels"],"summary":"Create a Booking.com rate plan and attach it to units","description":"Creates a new rate plan on Booking.com (name + optional derived relation) and attaches it to the listed units with a cancellation policy, meal plans and optional booking restrictions — the same two-step operation as the panel's Booking rate-plan dialog. Everything is validated against LIVE portal data before any write: units must exist on the hotel, `policyId` must be one of GET /channels/booking/policies, `derived.parentRatePlanId` must be an existing plan of the same hotel. `hotelId` is required only for multi-hotel accounts. After success the local mirror used by GET /channels is refreshed, so the new `ratePlanId` can be used immediately in PUT /channels/booking/pricing to send prices. If Booking.com creates the plan but rejects attaching units, the response is 502 and names the created `ratePlanId` — finish with PATCH instead of retrying POST (which would create a duplicate). Required scope: `channels.update`.","operationId":"createBookingRatePlan","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRatePlanWriteRequest"}}}},"responses":{"201":{"description":"Rate plan created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRatePlanResponse"}}}},"400":{"description":"Malformed JSON body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem400"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Hotel does not belong to this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"409":{"description":"Booking.com is not connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem409"}}}},"422":{"description":"Validation error (unknown fields, unknown units/policy/parent plan)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"Booking.com rejected the operation (details and RUIDs in the problem body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Delete a Booking.com rate plan (detach it from all units and deactivate it)

> Removes the rate plan from every unit it is sold on and then deactivates the plan itself on Booking.com (product Remove + rate-plan Remove), so its name becomes reusable — plain unit-detach alone would keep the name reserved and a later POST with the same name would fail with 'Rate with the same name already exists'. The plan must exist on one of the connected hotels. Reservations already made on this plan are not affected. After success the local mirror used by GET /channels is refreshed. This is DESTRUCTIVE and cannot be undone through this API — the plan would have to be recreated. Required scope: \`channels.update\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem409":{"description":"Conflict with resource state or integrity constraints.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"409 Conflict","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"}}},"paths":{"/channels/booking/rate-plans/{ratePlanId}":{"delete":{"tags":["channels"],"summary":"Delete a Booking.com rate plan (detach it from all units and deactivate it)","description":"Removes the rate plan from every unit it is sold on and then deactivates the plan itself on Booking.com (product Remove + rate-plan Remove), so its name becomes reusable — plain unit-detach alone would keep the name reserved and a later POST with the same name would fail with 'Rate with the same name already exists'. The plan must exist on one of the connected hotels. Reservations already made on this plan are not affected. After success the local mirror used by GET /channels is refreshed. This is DESTRUCTIVE and cannot be undone through this API — the plan would have to be recreated. Required scope: `channels.update`.","operationId":"deleteBookingRatePlan","parameters":[{"name":"ratePlanId","in":"path","description":"Booking.com rate plan id (GET /channels/booking/rate-plans).","required":true,"schema":{"type":"integer"}}],"responses":{"204":{"description":"Rate plan removed"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Rate plan not found on the connected hotels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"409":{"description":"Booking.com is not connected / plan has no units attached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem409"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"Booking.com rejected the operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Update a Booking.com rate plan (name, units, policy, meals, derived relation, restrictions)

> Updates an existing rate plan on Booking.com (Overlay — full replace of the plan configuration, like the panel dialog). Every field is optional: omitted \`name\`, \`unitIds\`, \`policyId\`, \`mealPlanCodes\` and \`derived\` keep their current LIVE values; \`derived: null\` detaches the plan from its parent. CAVEAT: \`minAdvanceReservation\`, \`maxAdvanceReservation\` and \`activeWeekdays\` cannot be read back from Booking.com — they are applied only when sent in this request and reset otherwise (panel behaviour). The plan must exist on one of the connected hotels (404 otherwise); all references are validated live as in POST. Prices per date are NOT set here — use PUT /channels/booking/pricing. Required scope: \`channels.update\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"BookingRatePlanUpdateRequest":{"description":"Partial update — every field is optional. Omitted fields keep their current live values (name, unitIds, policyId, mealPlanCodes, derived), EXCEPT minAdvanceReservation/maxAdvanceReservation/activeWeekdays which cannot be read back from Booking.com: they are applied only when sent and cleared otherwise (same behaviour as the panel dialog). `derived`: omitted = keep current, null = detach from parent.","properties":{"name":{"description":"Rate plan name shown on Booking.com.","type":"string","maxLength":255},"hotelId":{"description":"Required only when the account has more than one Booking.com hotel.","type":"integer","nullable":true},"unitIds":{"description":"Booking.com unit (room) ids to sell this plan on — from GET /channels or GET /channel-manager.","type":"array","items":{"type":"string"},"maxItems":50,"minItems":1},"policyId":{"description":"Cancellation policy id (GET /channels/booking/policies).","type":"integer"},"mealPlanCodes":{"description":"Booking.com meal plan codes: 0 None, 19 Breakfast, 21 Lunch, 22 Dinner, 10 Full board, 12 Half board, 1 All inclusive, … Empty list = no meals.","type":"array","items":{"type":"integer"}},"derived":{"oneOf":[{"$ref":"#/components/schemas/BookingRatePlanDerived"}],"nullable":true,"description":"Derived-plan config; null (or omitted on POST) = standalone plan. On PATCH: omitted = keep current, null = detach from parent."},"minAdvanceReservation":{"description":"Minimum advance before check-in to book (P1D = 1 day, P12H = 12 hours).","type":"string","pattern":"^P[0-9]{1,3}(Y|M|D|H)$","nullable":true},"maxAdvanceReservation":{"description":"Maximum advance before check-in to book.","type":"string","pattern":"^P[0-9]{1,3}(Y|M|D|H)$","nullable":true},"activeWeekdays":{"description":"Check-in weekdays the plan is bookable on; omitted = all days.","type":"array","items":{"type":"string","enum":["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]},"nullable":true}},"type":"object"},"BookingRatePlanDerived":{"description":"Derived (child) rate plan configuration: prices follow the parent plan with a percentage change. null = standalone plan.","required":["parentRatePlanId","priceChangePercent"],"properties":{"parentRatePlanId":{"description":"Parent Booking.com rate plan id (must exist on the same hotel).","type":"integer"},"priceChangePercent":{"description":"-10 = 10% cheaper than the parent, 20 = 20% more expensive (Booking.com Percentage 1-200).","type":"integer","maximum":100,"minimum":-99},"followsPrice":{"description":"Child follows parent price changes (default true).","type":"boolean"},"followsRestrictions":{"description":"Child follows parent restrictions (default false).","type":"boolean"},"followsPolicyGroup":{"description":"Child follows parent cancellation policy (default false).","type":"boolean"},"followsClosed":{"description":"Booking.com FollowsClosed mode (0 = independent).","type":"integer","maximum":3,"minimum":0}},"type":"object"},"BookingRatePlanResponse":{"required":["data"],"properties":{"data":{"required":["ratePlan"],"properties":{"ratePlan":{"$ref":"#/components/schemas/BookingRatePlanEntry"}},"type":"object"},"meta":{"properties":{"ruids":{"type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"},"BookingRatePlanEntry":{"description":"One Booking.com rate plan read LIVE from the portal, with the units (rooms) it is sold on.","required":["ratePlanId","name","hotelId","hotelName","policyId","policyName","mealPlanCodes","mealPlans","derived","units"],"properties":{"ratePlanId":{"description":"Booking.com rate plan id — the same id as `ratePlanId` in GET/PUT /channels/booking/pricing.","type":"integer"},"name":{"type":"string"},"hotelId":{"type":"integer"},"hotelName":{"type":"string"},"policyId":{"description":"Cancellation policy id (GET /channels/booking/policies).","type":"integer","nullable":true},"policyName":{"type":"string","nullable":true},"mealPlanCodes":{"description":"Booking.com meal plan codes (19 = Breakfast; see POST request schema for the full list).","type":"array","items":{"type":"integer"}},"mealPlans":{"type":"array","items":{"type":"string"}},"derived":{"oneOf":[{"$ref":"#/components/schemas/BookingRatePlanDerived"}],"nullable":true},"units":{"type":"array","items":{"$ref":"#/components/schemas/BookingRatePlanUnit"}}},"type":"object"},"BookingRatePlanUnit":{"required":["unitId","unitName","mappedRoomIds"],"properties":{"unitId":{"description":"Booking.com room (unit) id — the same id as `unitId` in GET /channels and GET/PUT /channels/booking/pricing.","type":"string"},"unitName":{"type":"string"},"mappedRoomIds":{"description":"mobile-calendar rooms mapped to this unit (GET /rooms); empty = unit not mapped in the channel manager.","type":"array","items":{"type":"integer"}}},"type":"object"},"ApiProblem400":{"description":"Request syntax/format error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"400 Bad Request","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem409":{"description":"Conflict with resource state or integrity constraints.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"409 Conflict","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"}}},"paths":{"/channels/booking/rate-plans/{ratePlanId}":{"patch":{"tags":["channels"],"summary":"Update a Booking.com rate plan (name, units, policy, meals, derived relation, restrictions)","description":"Updates an existing rate plan on Booking.com (Overlay — full replace of the plan configuration, like the panel dialog). Every field is optional: omitted `name`, `unitIds`, `policyId`, `mealPlanCodes` and `derived` keep their current LIVE values; `derived: null` detaches the plan from its parent. CAVEAT: `minAdvanceReservation`, `maxAdvanceReservation` and `activeWeekdays` cannot be read back from Booking.com — they are applied only when sent in this request and reset otherwise (panel behaviour). The plan must exist on one of the connected hotels (404 otherwise); all references are validated live as in POST. Prices per date are NOT set here — use PUT /channels/booking/pricing. Required scope: `channels.update`.","operationId":"updateBookingRatePlan","parameters":[{"name":"ratePlanId","in":"path","description":"Booking.com rate plan id (GET /channels/booking/rate-plans).","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRatePlanUpdateRequest"}}}},"responses":{"200":{"description":"Rate plan updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRatePlanResponse"}}}},"400":{"description":"Malformed JSON body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem400"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Rate plan not found on the connected hotels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"409":{"description":"Booking.com is not connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem409"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"Booking.com rejected the operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Get Booking.com cancellation policies (needed to create/update rate plans)

> Lists the cancellation policies configured on the connected Booking.com hotel(s), live from the portal. Use \`policyId\` as the cancellation policy when creating or updating a rate plan (POST/PATCH /channels/booking/rate-plans). Policies themselves are managed in the Booking.com extranet — this endpoint is read-only. Required scope: \`channels.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"BookingPoliciesResponse":{"required":["data"],"properties":{"data":{"required":["policies"],"properties":{"policies":{"type":"array","items":{"$ref":"#/components/schemas/BookingPolicyEntry"}}},"type":"object"},"meta":{"properties":{"total":{"type":"integer"},"ruids":{"type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"},"BookingPolicyEntry":{"required":["hotelId","policyId","groupName","type","cancelPenaltyCode","description","noShowPolicy"],"properties":{"hotelId":{"type":"integer"},"policyId":{"description":"Use as `policyId` when creating/updating a rate plan.","type":"integer"},"groupName":{"type":"string"},"type":{"type":"string"},"cancelPenaltyCode":{"description":"Booking.com cancellation penalty code.","type":"string"},"description":{"type":"string"},"noShowPolicy":{"type":"string"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem409":{"description":"Conflict with resource state or integrity constraints.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"409 Conflict","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"}}},"paths":{"/channels/booking/policies":{"get":{"tags":["channels"],"summary":"Get Booking.com cancellation policies (needed to create/update rate plans)","description":"Lists the cancellation policies configured on the connected Booking.com hotel(s), live from the portal. Use `policyId` as the cancellation policy when creating or updating a rate plan (POST/PATCH /channels/booking/rate-plans). Policies themselves are managed in the Booking.com extranet — this endpoint is read-only. Required scope: `channels.read`.","operationId":"getBookingPolicies","parameters":[{"name":"hotelId","in":"query","description":"Booking.com hotel id; omitted = all connected hotels.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Cancellation policies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingPoliciesResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Hotel does not belong to this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"409":{"description":"Booking.com is not connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem409"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"Booking.com did not return the data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Get all channel-manager connections (Booking.com, Airbnb, Slowhop, iCalendar) with room mappings

> Real-time overview of every connection between this account and external portals: the three direct API channels (Booking.com, Airbnb, Slowhop — always listed, \`connected: false\` when not linked) with the portal accounts/properties (\`accounts\`: Booking hotel id, Airbnb host accounts with e-mail and OAuth state, Slowhop place id), channel- and unit-level sync switches, whether prices are synced (\`supportsPriceSync\` — Slowhop syncs availability only), and the full room mapping (\`rooms\`: PMS \`roomId\`+\`roomName\` ↔ portal unit \`otaRoomId\`+\`otaRoomName\`; one portal unit can map to several PMS rooms). Also lists iCalendar feeds: imports pulled into rooms (with source and last sync time) and the per-room export URLs. Use \`otaRoomId\` as \`unitId\` in GET/PUT /channels/{channel}/pricing and \`sourceId\` to filter reservations by portal. Only active PMS rooms are included. Required scope: \`channelManager.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ChannelManagerResponse":{"required":["data","meta"],"properties":{"data":{"required":["channels","icalendar"],"properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelManagerChannel"}},"icalendar":{"$ref":"#/components/schemas/ChannelManagerIcal"}},"type":"object"},"meta":{"type":"object","allOf":[{"$ref":"#/components/schemas/MetaWithRuid"},{"properties":{"connectedChannels":{"description":"Number of direct channels with `connected: true`.","type":"integer"}},"type":"object"}]}},"type":"object"},"ChannelManagerChannel":{"description":"One direct (API) channel-manager connection. Every channel is always listed — `connected: false` means the portal is not linked to this account. Room mappings may remain listed after a portal was disconnected (historic state) — treat them as inactive whenever `connected` or `syncEnabled` is false.","required":["channel","name","sourceId","connectionType","refreshTime","connected","syncEnabled","supportsPriceSync","accounts","rooms"],"properties":{"channel":{"type":"string","enum":["booking","airbnb","slowhop","szallas"]},"name":{"type":"string"},"sourceId":{"description":"Reservation source id of the portal (`sourceId` in GET /reservations and GET /sources): 3 = Booking.com, 4 = Airbnb, 7789 = Slowhop. Szállás Group has its own source id (the channel covers all group portals: nocowanie.pl, noclegi.pl, szallas.hu, hotely.cz, spa.cz, travelminit.ro, litoralulromanesc.ro, revngo.com, maiutazas.hu) — a reservation is stored under the portal it came from, so its `zrodlo` may differ from the channel source id.","type":"integer"},"connectionType":{"description":"`direct` = live API connection (reservations, availability and — where supported — prices sync in real time).","type":"string"},"refreshTime":{"type":"string"},"connected":{"description":"The portal is linked (Booking: property connected; Airbnb: at least one active host account; Slowhop: at least one mapped object).","type":"boolean"},"syncEnabled":{"description":"Channel-level sync switch — false = nothing is pushed/imported for this portal even when connected.","type":"boolean"},"supportsPriceSync":{"description":"false = only availability is synced (Slowhop has no prices API); prices for supported portals are managed via GET/PUT /channels/{channel}/pricing.","type":"boolean"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/ChannelManagerAccount"}},"rooms":{"description":"Room mappings (active PMS rooms only).","type":"array","items":{"$ref":"#/components/schemas/ChannelManagerRoom"}}},"type":"object"},"ChannelManagerAccount":{"description":"Account/property connected on the portal side: Booking.com → property (hotel) id, Airbnb → host account (one PMS account can connect several Airbnb hosts), Slowhop → place id.","required":["accountId","name","email","active"],"properties":{"accountId":{"description":"Portal identifier: Booking.com hotel id / Airbnb host account id (airbnbUserId) / Slowhop place id.","type":"string"},"name":{"description":"Airbnb: host first name; Booking/Slowhop: generated label.","type":"string","nullable":true},"email":{"description":"Airbnb host e-mail; null for other portals.","type":"string","nullable":true},"active":{"description":"Airbnb: OAuth connection active (false = disconnected/expired account, its listings stop syncing).","type":"boolean"}},"type":"object"},"ChannelManagerRoom":{"description":"Mapping of a PMS room to a portal unit. One portal unit (Booking room / Airbnb listing) may map to SEVERAL PMS rooms (room-type / group inventory) — such rows share `otaRoomId`.","required":["roomId","roomName","otaRoomId","otaRoomName","accountId","syncEnabled"],"properties":{"roomId":{"description":"mobile-calendar room (GET /rooms).","type":"integer"},"roomName":{"type":"string"},"otaRoomId":{"description":"Portal unit id: Booking.com room id / Airbnb listing id / Slowhop object id. The same id is `unitId` in GET /channels (OTA pricing).","type":"string"},"otaRoomName":{"description":"Unit name on the portal (Airbnb listing name, Slowhop object name); null for Booking.com.","type":"string","nullable":true},"accountId":{"description":"Which portal account/property the unit belongs to (see `accounts`).","type":"string"},"syncEnabled":{"description":"Per-unit sync switch (Airbnb listing / Slowhop object); null for Booking.com — there only the channel-level `syncEnabled` exists.","type":"boolean","nullable":true},"pricingModel":{"description":"Airbnb only: pricing model of the listing (see GET /channels).","type":"string","enum":["STANDARD","LOS_RECORD","RATE_PLAN"],"nullable":true}},"type":"object"},"ChannelManagerIcal":{"description":"iCalendar connections — one-way calendar feeds for portals without a direct API. `imports` = external calendars pulled into rooms (cron every 10–30 min), `exports` = this account's per-room calendar URLs to paste into other portals.","required":["connectionType","refreshTime","imports","exports"],"properties":{"connectionType":{"type":"string"},"refreshTime":{"type":"string"},"imports":{"type":"array","items":{"$ref":"#/components/schemas/ChannelManagerIcalImport"}},"exports":{"type":"array","items":{"required":["roomId","roomName","url"],"properties":{"roomId":{"type":"integer"},"roomName":{"type":"string"},"url":{"description":"Public (token-protected) iCal feed of the room's reservations.","type":"string"}},"type":"object"}}},"type":"object"},"ChannelManagerIcalImport":{"required":["icalId","roomId","roomName","sourceId","sourceName","url","lastSyncAt"],"properties":{"icalId":{"type":"integer"},"roomId":{"type":"integer"},"roomName":{"type":"string"},"sourceId":{"description":"Reservation source assigned to imported bookings (GET /sources); 0 = other.","type":"integer"},"sourceName":{"type":"string","nullable":true},"url":{"description":"External calendar URL imported into the room.","type":"string"},"lastSyncAt":{"description":"Last successful import (UTC); null = never imported yet.","type":"string","format":"date-time","nullable":true}},"type":"object"},"MetaWithRuid":{"description":"Metadata with request tracking.","required":["ruid"],"properties":{"ruid":{"description":"Request Unique ID","type":"string","format":"uuid"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]}}},"paths":{"/channel-manager":{"get":{"tags":["channels"],"summary":"Get all channel-manager connections (Booking.com, Airbnb, Slowhop, iCalendar) with room mappings","description":"Real-time overview of every connection between this account and external portals: the three direct API channels (Booking.com, Airbnb, Slowhop — always listed, `connected: false` when not linked) with the portal accounts/properties (`accounts`: Booking hotel id, Airbnb host accounts with e-mail and OAuth state, Slowhop place id), channel- and unit-level sync switches, whether prices are synced (`supportsPriceSync` — Slowhop syncs availability only), and the full room mapping (`rooms`: PMS `roomId`+`roomName` ↔ portal unit `otaRoomId`+`otaRoomName`; one portal unit can map to several PMS rooms). Also lists iCalendar feeds: imports pulled into rooms (with source and last sync time) and the per-room export URLs. Use `otaRoomId` as `unitId` in GET/PUT /channels/{channel}/pricing and `sourceId` to filter reservations by portal. Only active PMS rooms are included. Required scope: `channelManager.read`.","operationId":"getChannelManager","responses":{"200":{"description":"Channel-manager status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelManagerResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}}}}}}}
```

## List Airbnb listings with their publication status

> Every listing of the connected Airbnb host accounts, read LIVE from Airbnb — the same list and status as the panel's Airbnb panel → Your listings: \`status\` \`LISTED\` (published, bookable on Airbnb) or \`UNLISTED\` (not published / snoozed by the host), plus the mobile-calendar rooms it is mapped to (\`roomIds\`, \`roomNames\`; \`mapped: false\` = not connected to any room) and \`coHosted\` (listing where this account is only a co-host). \`meta\` counts listed/unlisted and lists the host accounts (\`isConnected: false\` = the account's authorization expired — its listings are missing). Use it for 'which listings are unpublished'. Publishing or unpublishing is done in the panel (Airbnb panel → listing), not through this API. Required scope: \`channelManager.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]}}},"paths":{"/channel-manager/airbnb/listings":{"get":{"tags":["channels"],"summary":"List Airbnb listings with their publication status","description":"Every listing of the connected Airbnb host accounts, read LIVE from Airbnb — the same list and status as the panel's Airbnb panel → Your listings: `status` `LISTED` (published, bookable on Airbnb) or `UNLISTED` (not published / snoozed by the host), plus the mobile-calendar rooms it is mapped to (`roomIds`, `roomNames`; `mapped: false` = not connected to any room) and `coHosted` (listing where this account is only a co-host). `meta` counts listed/unlisted and lists the host accounts (`isConnected: false` = the account's authorization expired — its listings are missing). Use it for 'which listings are unpublished'. Publishing or unpublishing is done in the panel (Airbnb panel → listing), not through this API. Required scope: `channelManager.read`.","operationId":"listAirbnbListings","responses":{"200":{"description":"Airbnb listings","content":{"application/json":{"schema":{"required":["data","meta"],"properties":{"data":{"type":"array","items":{"properties":{"listingId":{"type":"string"},"name":{"type":"string","nullable":true},"status":{"description":"null when Airbnb did not report it.","type":"string","enum":["LISTED","UNLISTED"],"nullable":true},"airbnbUserId":{"type":"string","nullable":true},"mapped":{"type":"boolean"},"roomIds":{"type":"array","items":{"type":"integer"}},"roomNames":{"type":"array","items":{"type":"string"}},"coHosted":{"type":"boolean"},"personCapacity":{"type":"integer","nullable":true},"city":{"type":"string","nullable":true}},"type":"object"}},"meta":{"properties":{"total":{"type":"integer"},"listed":{"type":"integer"},"unlisted":{"type":"integer"},"accounts":{"type":"array","items":{"properties":{"airbnbUserId":{"type":"string"},"isConnected":{"type":"boolean"}},"type":"object"}}},"type":"object"}},"type":"object"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Airbnb is not connected on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}}}}}}}
```

## List connected OTA channels (Booking.com, Airbnb) with their units and rate plans

> Returns the OTA portals connected to the account and, per portal, its units: Booking.com rooms and Airbnb listings (\`unitId\`), the mobile-calendar rooms they are mapped to (\`roomIds\`), the portal rate plans (\`ratePlans\`), the pricing model of the unit and the exact price fields / restriction keys accepted by PUT /channels/{channel}/pricing. IMPORTANT: these are the portal's own prices, a separate price list from the mobile-calendar pricing (GET/PUT /pricing, rate plans from GET /rates). mobile-calendar prices reach a portal unit ONLY through an active price sync rule (GET /channels/price-sync: source rate × room → unit, with adjustment such as +55 PLN or +10%); units without a rule keep their own prices. Use this call first to learn \`unitId\`, \`ratePlanId\` and the supported fields. Required scope: \`channels.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ChannelsResponse":{"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChannelEntry"}},"meta":{"type":"object","allOf":[{"$ref":"#/components/schemas/MetaWithRuid"},{"properties":{"totalChannels":{"type":"integer"}},"type":"object"}]}},"type":"object"},"ChannelEntry":{"description":"A connected OTA portal with its units. Only connected portals are listed (Booking.com: property connected; Airbnb: at least one listing mapped).","required":["channel","name","syncEnabled","units"],"properties":{"channel":{"description":"Channel key used in the path `/channels/{channel}/pricing`.","type":"string","enum":["booking","airbnb","szallas"]},"name":{"type":"string"},"syncEnabled":{"description":"Channel manager sync with the portal is on.","type":"boolean"},"units":{"type":"array","items":{"$ref":"#/components/schemas/ChannelUnit"}}},"type":"object"},"ChannelUnit":{"description":"A unit of the portal: Booking.com room (`room_id`) or Airbnb listing. One unit maps to one or more mobile-calendar rooms (`roomIds`, room-type / group inventory). `priceFields` and `restrictions` list exactly the keys accepted by PUT pricing for this unit — they depend on the channel and, for Airbnb, on the listing's `pricingModel`.","required":["unitId","name","roomIds","hotelId","pricingModel","ratePlans","priceFields","restrictions"],"properties":{"unitId":{"description":"Portal identifier: Booking.com room id (numeric) or Airbnb listing id. Use it as `unitId` in GET/PUT pricing.","type":"string"},"name":{"type":"string"},"roomIds":{"description":"mobile-calendar rooms (GET /rooms) mapped to this unit.","type":"array","items":{"type":"integer"}},"hotelId":{"description":"Booking.com property (hotel) id; null for Airbnb.","type":"string","nullable":true},"pricingModel":{"description":"How the portal prices this unit. `RATE_PLAN` (Booking.com always; Airbnb optionally): prices per rate plan → `ratePlanId` required. `STANDARD` (Airbnb): one nightly `basePrice` + restrictions, no rate plans. `LOS_RECORD` (Airbnb length-of-stay): total price per number of nights (`lengthOfStayPrices`), no nightly price, no min/max stay (the curve defines allowed lengths).","type":"string","enum":["RATE_PLAN","STANDARD","LOS_RECORD"]},"inModelTransition":{"description":"Airbnb only: the listing is switching its pricing model — writes are rejected (422) until it finishes.","type":"boolean"},"syncEnabled":{"description":"Airbnb only: availability/price sync of the listing is on.","type":"boolean"},"ratePlans":{"description":"Rate plans of the unit (empty for Airbnb STANDARD / LOS_RECORD).","type":"array","items":{"$ref":"#/components/schemas/ChannelRatePlan"}},"priceFields":{"description":"Price keys accepted by PUT pricing for this unit.","type":"array","items":{"type":"string","enum":["basePrice","singleOccupancyPrice","lengthOfStayPrices"]}},"restrictions":{"description":"Restriction keys accepted by PUT pricing (`restrictions` object) for this unit.","type":"array","items":{"type":"string"}}},"type":"object"},"ChannelRatePlan":{"description":"Rate plan defined ON THE PORTAL (Booking.com rate / Airbnb rate plan) — not a mobile-calendar rate (GET /rates). Prices of a `derived` plan are calculated by the portal from its parent and cannot be set here (restrictions can).","required":["ratePlanId","name","maxPersons","derived","parentRatePlanId","rateRelation","pricingType","singleOccupancyPrice"],"properties":{"ratePlanId":{"description":"Portal rate plan id; use it as `ratePlanId` in GET/PUT pricing.","type":"string"},"name":{"type":"string"},"maxPersons":{"description":"Occupancy the `basePrice` refers to (Booking.com: rate max_persons; Airbnb: listing capacity).","type":"integer","nullable":true},"derived":{"description":"true = prices derived from `parentRatePlanId` by the portal (e.g. Booking.com child rate -10%).","type":"boolean"},"parentRatePlanId":{"type":"string","nullable":true},"rateRelation":{"description":"Derived plan relation: `{percentage, additional}` as on the portal.","properties":{"percentage":{"type":"number","nullable":true},"additional":{"type":"number","nullable":true}},"type":"object","nullable":true},"pricingType":{"description":"Portal pricing type of the plan. Booking.com: `Standard` (one price for max occupancy), `RLO` (Rate Level Occupancy — price per occupancy derived from `basePrice` by percentages configured on Booking.com), `OBP`/`LOS` variants. Airbnb: `Standard` or `OCCUPANCY_MODEL` (mobile-calendar occupancy pricing model expands `basePrice` into a per-guest ladder).","type":"string"},"singleOccupancyPrice":{"description":"Booking.com only: the plan has a separate single-occupancy price (`singleOccupancyPrice` accepted in PUT).","type":"boolean"}},"type":"object"},"MetaWithRuid":{"description":"Metadata with request tracking.","required":["ruid"],"properties":{"ruid":{"description":"Request Unique ID","type":"string","format":"uuid"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]}}},"paths":{"/channels":{"get":{"tags":["channels"],"summary":"List connected OTA channels (Booking.com, Airbnb) with their units and rate plans","description":"Returns the OTA portals connected to the account and, per portal, its units: Booking.com rooms and Airbnb listings (`unitId`), the mobile-calendar rooms they are mapped to (`roomIds`), the portal rate plans (`ratePlans`), the pricing model of the unit and the exact price fields / restriction keys accepted by PUT /channels/{channel}/pricing. IMPORTANT: these are the portal's own prices, a separate price list from the mobile-calendar pricing (GET/PUT /pricing, rate plans from GET /rates). mobile-calendar prices reach a portal unit ONLY through an active price sync rule (GET /channels/price-sync: source rate × room → unit, with adjustment such as +55 PLN or +10%); units without a rule keep their own prices. Use this call first to learn `unitId`, `ratePlanId` and the supported fields. Required scope: `channels.read`.","operationId":"listChannels","responses":{"200":{"description":"Connected channels (empty list when no portal is connected)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelsResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}}}}}}}
```

## Get OTA channel prices and restrictions

> Returns, per unit × rate plan, the prices and restrictions stored in the local mirror of the portal (the last values pushed to or imported from Booking.com / Airbnb) for a date range of up to 366 days. Booking.com: \`basePrice\` (max-occupancy price of the plan), \`singleOccupancyPrice\`, Booking.com restriction set. Airbnb STANDARD/RATE\_PLAN: \`basePrice\` + \`minStay\`/\`maxStay\`/\`availArrival\`/\`availDeparture\`/\`closed\`. Airbnb LOS\_RECORD: \`lengthOfStayPrices\` (total price per nights) + \`guests\` + \`closed\`. Dates with no stored value are omitted. These are the portal's prices — mobile-calendar prices are under GET /pricing. Required scope: \`channels.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ChannelPricingResponse":{"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChannelPricingEntry"}},"meta":{"type":"object","allOf":[{"$ref":"#/components/schemas/MetaWithRuid"},{"properties":{"channel":{"type":"string"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"totalUnits":{"type":"integer"}},"type":"object"}]}},"type":"object"},"ChannelPricingEntry":{"description":"Pricing of one unit × rate plan.","required":["unitId","roomIds","ratePlanId","pricingModel","dates"],"properties":{"unitId":{"type":"string"},"roomIds":{"type":"array","items":{"type":"integer"}},"ratePlanId":{"description":"null for Airbnb STANDARD / LOS_RECORD units.","type":"string","nullable":true},"pricingModel":{"type":"string","enum":["RATE_PLAN","STANDARD","LOS_RECORD"]},"dates":{"description":"Map date (YYYY-MM-DD) → prices/restrictions; dates without any stored value are omitted.","type":"object","additionalProperties":{"$ref":"#/components/schemas/ChannelDatePricing"}}},"type":"object"},"ChannelDatePricing":{"description":"Prices and restrictions of one date as stored in the local mirror of the portal (last value pushed to / imported from the portal).","required":["basePrice","restrictions"],"properties":{"basePrice":{"description":"Nightly price for the unit (Booking.com: price for `maxPersons` of the plan; Airbnb STANDARD/RATE_PLAN: nightly price; Airbnb LOS_RECORD: 1-night total, informational). null = no price on this date.","type":"number","format":"float","nullable":true},"singleOccupancyPrice":{"description":"Booking.com only: price for 1 person (`price1`).","type":"number","format":"float","nullable":true},"lengthOfStayPrices":{"description":"Airbnb LOS_RECORD only: TOTAL stay price per number of nights (key = nights).","type":"object","additionalProperties":{"type":"number"}},"guests":{"description":"Airbnb LOS_RECORD only: number of guests the curve is priced for.","type":"integer"},"restrictions":{"$ref":"#/components/schemas/ChannelRestrictions"}},"type":"object"},"ChannelRestrictions":{"description":"Restrictions for a date, in the portal's own semantics. Common keys: `minStay`/`maxStay` (nights; 0 = no limit), `availArrival`/`availDeparture` (arrival/departure allowed — false = closed to arrival/departure), `closed` (date not bookable). Booking.com only: `minimumStayArrival`/`maximumStayArrival`/`exactStayArrival` (stay length counted from arrival on this date), `minAdvanceRes`/`maxAdvanceRes` (lead time `1D`, `12H`, `2D12H`; empty = no limit). Airbnb STANDARD/RATE_PLAN: `minStay`/`maxStay` (RATE_PLAN max 28), `availArrival`, `availDeparture`, `closed`. Airbnb LOS_RECORD: only `closed`. Keys not present = not set on that date.","properties":{"minStay":{"type":"integer","minimum":0},"maxStay":{"type":"integer","minimum":0},"availArrival":{"type":"boolean"},"availDeparture":{"type":"boolean"},"closed":{"type":"boolean"},"minimumStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"maximumStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"exactStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"minAdvanceRes":{"description":"Booking.com","type":"string","pattern":"^(\\d{1,3}D)?(\\d{1,2}H)?$"},"maxAdvanceRes":{"description":"Booking.com","type":"string","pattern":"^(\\d{1,3}D)?(\\d{1,2}H)?$"}},"type":"object"},"MetaWithRuid":{"description":"Metadata with request tracking.","required":["ruid"],"properties":{"ruid":{"description":"Request Unique ID","type":"string","format":"uuid"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]}}},"paths":{"/channels/{channel}/pricing":{"get":{"tags":["channels"],"summary":"Get OTA channel prices and restrictions","description":"Returns, per unit × rate plan, the prices and restrictions stored in the local mirror of the portal (the last values pushed to or imported from Booking.com / Airbnb) for a date range of up to 366 days. Booking.com: `basePrice` (max-occupancy price of the plan), `singleOccupancyPrice`, Booking.com restriction set. Airbnb STANDARD/RATE_PLAN: `basePrice` + `minStay`/`maxStay`/`availArrival`/`availDeparture`/`closed`. Airbnb LOS_RECORD: `lengthOfStayPrices` (total price per nights) + `guests` + `closed`. Dates with no stored value are omitted. These are the portal's prices — mobile-calendar prices are under GET /pricing. Required scope: `channels.read`.","operationId":"getChannelPricing","parameters":[{"name":"channel","in":"path","description":"Channel key from GET /channels","required":true,"schema":{"type":"string","enum":["booking","airbnb","szallas"]}},{"name":"from","in":"query","description":"First date (YYYY-MM-DD)","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","description":"Last date, inclusive (max 366 days after `from`)","required":true,"schema":{"type":"string","format":"date"}},{"name":"unitId","in":"query","description":"Unit id(s) from GET /channels, comma-separated; omitted = all units of the channel","required":false,"schema":{"type":"string"}},{"name":"ratePlanId","in":"query","description":"Rate plan id(s), comma-separated; omitted = all plans of the selected units (ignored for Airbnb STANDARD/LOS_RECORD)","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pricing per unit × rate plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPricingResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Channel not connected or unknown unit id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"422":{"description":"Validation error — missing/invalid dates, range too long, unknown rate plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}}}}}}}
```

## Update OTA channel prices and restrictions (push to the portal)

> Applies prices/restrictions to the selected dates of one or more units of the channel: the change is sent to the portal FIRST and the local mirror is updated only when the portal accepts it (502 with per-unit \`results\` otherwise — nothing is stored locally for rejected units). Values are merged per date — send only the fields to change. Rules: \`ratePlanId\` is required for Booking.com and Airbnb RATE\_PLAN units (from GET /channels) and must belong to every selected unit; prices cannot be set on a derived plan (portal calculates them from the parent; restrictions are allowed); accepted price fields and restriction keys are exactly \`priceFields\`/\`restrictions\` of the unit in GET /channels (Booking.com: \`basePrice\`, \`singleOccupancyPrice\` when the plan has it; Airbnb STANDARD/RATE\_PLAN: \`basePrice\`, Airbnb LOS\_RECORD: \`lengthOfStayPrices\`+\`guests\`); \`minStay\`/\`maxStay\` 0 = remove the limit (Airbnb RATE\_PLAN max 28 nights); \`minAdvanceRes\`/\`maxAdvanceRes\` (Booking.com) as \`1D\`/\`12H\`/\`2D12H\`, empty = no limit; past dates are rejected; max 20 units, 366 dates/days per call. Booking.com processes the update asynchronously after accepting it (a few minutes until visible in the extranet). This does NOT change mobile-calendar prices (PUT /pricing). Required scope: \`channels.update\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"ChannelPricingUpdateRequest":{"description":"Prices/restrictions to apply to the selected dates of one or more units (max 20) of the channel. Dates: either `dates` (list, max 366) or `startDate`+`stopDate` (max 366 days) optionally filtered by `days` (weekdays). Dates in the past are rejected. Send only what should change — values are merged per date; at least one price field or restriction is required. Which price fields / restriction keys are accepted depends on the unit (see `priceFields` / `restrictions` in GET /channels): Booking.com → `basePrice`, `singleOccupancyPrice` (if the plan has it) and the Booking.com restriction set; Airbnb STANDARD/RATE_PLAN → `basePrice` + `minStay`/`maxStay`/`availArrival`/`availDeparture`/`closed`; Airbnb LOS_RECORD → `lengthOfStayPrices` (+`guests`) and `closed`. `ratePlanId` is required for Booking.com and Airbnb RATE_PLAN units and must belong to every selected unit; prices cannot be set on a derived plan.","required":["unitId"],"properties":{"unitId":{"description":"Unit ids from GET /channels (Booking.com room ids / Airbnb listing ids) of this channel; a single id may also be sent as a plain string. All units get the same values.","type":"array","items":{"type":"string"},"maxItems":20,"minItems":1},"ratePlanId":{"description":"Portal rate plan id (from GET /channels). Required for Booking.com and Airbnb RATE_PLAN; must be omitted for Airbnb STANDARD/LOS_RECORD.","type":"string","nullable":true},"startDate":{"type":"string","format":"date"},"stopDate":{"description":"Inclusive.","type":"string","format":"date"},"days":{"description":"Weekdays to apply within the range: 0 = Sunday … 6 = Saturday. Omitted = all days.","type":"array","items":{"type":"integer","maximum":6,"minimum":0}},"dates":{"description":"Explicit dates instead of a range.","type":"array","items":{"type":"string","format":"date"},"maxItems":366},"basePrice":{"description":"Nightly price (account currency). Booking.com: price for the plan's max occupancy; Airbnb: nightly price (for RATE_PLAN with an occupancy pricing model it is the base the per-guest ladder is calculated from).","type":"number","format":"float","minimum":0},"singleOccupancyPrice":{"description":"Booking.com only: 1-person price of plans that have it.","type":"number","format":"float","minimum":0},"lengthOfStayPrices":{"description":"Airbnb LOS_RECORD only: TOTAL stay price per number of nights (key = nights, 1–365). Merged into the existing curve per date; 0 or null removes that threshold.","type":"object","additionalProperties":{"type":"number","minimum":0}},"guests":{"description":"Airbnb LOS_RECORD only: guests the curve is priced for.","type":"integer","maximum":20,"minimum":1},"restrictions":{"$ref":"#/components/schemas/ChannelRestrictions"}},"type":"object"},"ChannelRestrictions":{"description":"Restrictions for a date, in the portal's own semantics. Common keys: `minStay`/`maxStay` (nights; 0 = no limit), `availArrival`/`availDeparture` (arrival/departure allowed — false = closed to arrival/departure), `closed` (date not bookable). Booking.com only: `minimumStayArrival`/`maximumStayArrival`/`exactStayArrival` (stay length counted from arrival on this date), `minAdvanceRes`/`maxAdvanceRes` (lead time `1D`, `12H`, `2D12H`; empty = no limit). Airbnb STANDARD/RATE_PLAN: `minStay`/`maxStay` (RATE_PLAN max 28), `availArrival`, `availDeparture`, `closed`. Airbnb LOS_RECORD: only `closed`. Keys not present = not set on that date.","properties":{"minStay":{"type":"integer","minimum":0},"maxStay":{"type":"integer","minimum":0},"availArrival":{"type":"boolean"},"availDeparture":{"type":"boolean"},"closed":{"type":"boolean"},"minimumStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"maximumStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"exactStayArrival":{"description":"Booking.com","type":"integer","minimum":0},"minAdvanceRes":{"description":"Booking.com","type":"string","pattern":"^(\\d{1,3}D)?(\\d{1,2}H)?$"},"maxAdvanceRes":{"description":"Booking.com","type":"string","pattern":"^(\\d{1,3}D)?(\\d{1,2}H)?$"}},"type":"object"},"ChannelPricingUpdateResponse":{"required":["data","meta"],"properties":{"data":{"required":["channel","results"],"properties":{"channel":{"type":"string"},"results":{"type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"},"meta":{"type":"object","allOf":[{"$ref":"#/components/schemas/MetaWithRuid"},{"properties":{"totalUnits":{"type":"integer"}},"type":"object"}]}},"type":"object"},"ChannelPricingResult":{"required":["unitId","ratePlanId","ok","httpCode","message","dates"],"properties":{"unitId":{"type":"string"},"ratePlanId":{"type":"string","nullable":true},"ok":{"description":"The portal accepted the change and the local mirror was updated.","type":"boolean"},"httpCode":{"description":"HTTP status returned by the portal.","type":"integer"},"message":{"description":"Portal message (error details when `ok` is false).","type":"string"},"dates":{"description":"Number of dates affected.","type":"integer"}},"type":"object"},"MetaWithRuid":{"description":"Metadata with request tracking.","required":["ruid"],"properties":{"ruid":{"description":"Request Unique ID","type":"string","format":"uuid"}},"type":"object"},"ApiProblem400":{"description":"Request syntax/format error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"400 Bad Request","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem404":{"description":"Resource specified in the request was not found.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"404 Not Found","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]},"ApiProblem502":{"description":"The external portal (OTA) did not accept the change; nothing was saved locally for the failed units.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"title":{"description":"502 Bad Gateway (OTA portal rejected the request)","type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"channel":{"type":"string"},"results":{"description":"Outcome per unit — `ok: true` entries WERE applied (partial success is possible when several units are sent).","type":"array","items":{"$ref":"#/components/schemas/ChannelPricingResult"}}},"type":"object"}]}}},"paths":{"/channels/{channel}/pricing":{"put":{"tags":["channels"],"summary":"Update OTA channel prices and restrictions (push to the portal)","description":"Applies prices/restrictions to the selected dates of one or more units of the channel: the change is sent to the portal FIRST and the local mirror is updated only when the portal accepts it (502 with per-unit `results` otherwise — nothing is stored locally for rejected units). Values are merged per date — send only the fields to change. Rules: `ratePlanId` is required for Booking.com and Airbnb RATE_PLAN units (from GET /channels) and must belong to every selected unit; prices cannot be set on a derived plan (portal calculates them from the parent; restrictions are allowed); accepted price fields and restriction keys are exactly `priceFields`/`restrictions` of the unit in GET /channels (Booking.com: `basePrice`, `singleOccupancyPrice` when the plan has it; Airbnb STANDARD/RATE_PLAN: `basePrice`, Airbnb LOS_RECORD: `lengthOfStayPrices`+`guests`); `minStay`/`maxStay` 0 = remove the limit (Airbnb RATE_PLAN max 28 nights); `minAdvanceRes`/`maxAdvanceRes` (Booking.com) as `1D`/`12H`/`2D12H`, empty = no limit; past dates are rejected; max 20 units, 366 dates/days per call. Booking.com processes the update asynchronously after accepting it (a few minutes until visible in the extranet). This does NOT change mobile-calendar prices (PUT /pricing). Required scope: `channels.update`.","operationId":"setChannelPricing","parameters":[{"name":"channel","in":"path","description":"Channel key from GET /channels","required":true,"schema":{"type":"string","enum":["booking","airbnb","szallas"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPricingUpdateRequest"}}}},"responses":{"200":{"description":"Accepted by the portal and stored locally","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelPricingUpdateResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem400"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"404":{"description":"Channel not connected or unknown unit id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem404"}}}},"422":{"description":"Validation error — unsupported field for the unit/model, missing `ratePlanId`, price on a derived plan, dates in the past, listing in model transition; for Booking.com also contradictory restrictions after a save that changes min/max on a date (`MIN_STAY_ABOVE_MAX_STAY` / `MIN_ADVANCE_ABOVE_MAX_ADVANCE`, counting values already stored for the date; re-sending unchanged values is not rejected — nothing is pushed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}},"502":{"description":"The portal rejected the update (per-unit `results`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem502"}}}}}}}}}
```

## Price sync rules: how mobile-calendar prices are pushed to OTA portals

> Lists the account's price sync rules (panel: Pricing → Price sync). A rule maps ONE source — a mobile-calendar rate plan × room (or room type) — to ONE target: a portal unit (Booking.com room, Airbnb listing, Szállás room) × portal rate plan, with an adjustment (\`PERCENT\`/\`AMOUNT\`, signed: +55 = +55 PLN, -10 = -10%) and rounding. While a rule is ACTIVE, every change of the source price in the mobile-calendar pricing (PUT /pricing, bulk changes) is pushed to the portal automatically, after the adjustment and rounding, usually within a minute; a PAUSED rule pushes nothing. A price of 0 in mobile-calendar closes the date on the portal. \`lastRun\` = the last recalculation/push of the rule (panel: "Last recalculation") with its status. PREVIEW: pass \`price\` (a mobile-calendar price) — every returned rule gets \`preview\.channelPrice\`, the exact amount that would be sent to the portal (same calculation as the sync worker). Use it to answer "what will the price be on Airbnb/Booking if I set X": filter by \`roomId\` (or \`typeId\`) and \`rateId\` of the mobile-calendar price being changed. No rule for a unit = changing the mobile-calendar price does not change the portal price. Required scope: \`channels.read\`.

```json
{"openapi":"3.0.0","info":{"title":"Public REST API v1 – mobile-calendar.com","version":"1.0.0"},"tags":[{"name":"channels","description":"OTA channel pricing (Booking.com, Airbnb): connected units, rate plans, prices and restrictions per portal — separate from the mobile-calendar pricing (/pricing)."}],"servers":[{"url":"https://api.mobile-calendar.com/v1/public","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","description":"Pass the token in header: Authorization: Bearer <JWT>","bearerFormat":"JWT","scheme":"bearer"}},"schemas":{"PriceSyncRulesResponse":{"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PriceSyncRule"}},"meta":{"properties":{"totalRules":{"type":"integer"},"activeRules":{"type":"integer"},"ruid":{"type":"string"}},"type":"object"}},"type":"object"},"PriceSyncRule":{"required":["ruleId","channel","active","status","source","target","adjustment","syncRestrictions","currency","lastRun"],"properties":{"ruleId":{"type":"integer"},"channel":{"type":"string","enum":["booking","airbnb","szallas"]},"active":{"description":"true = price changes are pushed; false = rule paused (nothing is pushed)","type":"boolean"},"status":{"type":"string","enum":["ACTIVE","PAUSED"]},"source":{"description":"mobile-calendar side: rate plan × room or room type","properties":{"rateId":{"type":"integer"},"rateName":{"type":"string","nullable":true},"roomId":{"type":"integer","nullable":true},"roomName":{"type":"string","nullable":true},"typeId":{"type":"integer","nullable":true},"typeName":{"type":"string","nullable":true},"roomIds":{"description":"Rooms priced by this source","type":"array","items":{"type":"integer"}}},"type":"object"},"target":{"description":"Portal side","properties":{"unitId":{"description":"Booking room id / Airbnb listing id / Szállás room id (as in GET /channels)","type":"string"},"unitName":{"type":"string","nullable":true},"ratePlanId":{"description":"Portal rate plan; null = Airbnb STANDARD calendar","type":"string","nullable":true},"ratePlanName":{"type":"string","nullable":true},"model":{"type":"string","nullable":true}},"type":"object"},"adjustment":{"properties":{"type":{"type":"string","enum":["NONE","PERCENT","AMOUNT"]},"value":{"description":"Signed: 55 = +55 (PLN or %), -10 = -10","type":"number","format":"float"},"rounding":{"type":"string","enum":["NONE","INT","HALF","FIVE","TEN","NINETY_NINE"]},"label":{"type":"string"}},"type":"object"},"syncRestrictions":{"description":"Restrictions (min/max stay, closed to arrival/departure…) are synced too","type":"boolean"},"currency":{"type":"string"},"lastRun":{"description":"Last recalculation/push of the rule (panel: \"Last recalculation\")","properties":{"at":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","enum":["OK","WARN","ERROR"],"nullable":true},"message":{"type":"string","nullable":true}},"type":"object"},"preview":{"description":"Present only when `price` was sent","properties":{"price":{"description":"mobile-calendar price from the request","type":"number","format":"float"},"sent":{"description":"false = nothing would be pushed (rule paused or price not pushable)","type":"boolean"},"channelPrice":{"description":"Price that would be sent to the portal (after adjustment and rounding); null when the date would be closed or nothing is sent","type":"number","format":"float","nullable":true},"closed":{"description":"The date would be closed on the portal (price 0 in mobile-calendar)","type":"boolean"},"reason":{"description":"RULE_PAUSED, PRICE_NON_POSITIVE_AFTER_ADJUSTMENT, PRICE_ZERO_CLOSES_DATE","type":"string","nullable":true}},"type":"object","nullable":true}},"type":"object"},"ApiProblem401":{"description":"Missing authentication or invalid credentials.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"401 Unauthorized","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblemBase":{"description":"RFC 7807 Problem Details (base schema).","required":["type","title","status","detail","instance"],"properties":{"type":{"description":"RFC 7807 base — DO NOT use directly in endpoints.\nIn endpoints reference ApiProblem400/401/403/409/422/429/500.","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"},"ApiProblem403":{"description":"Authenticated user but no access.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"403 Forbidden","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"type":"object"}]},"ApiProblem422":{"description":"Input data validation error.","allOf":[{"$ref":"#/components/schemas/ApiProblemBase"},{"properties":{"type":{"description":"422 Unprocessable Entity (validation)","type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"description":"Map: field name → error message(s)","type":"object","additionalProperties":{"$ref":"#/components/schemas/ErrorValue"}}},"type":"object"}]},"ErrorValue":{"description":"Single error message or list of messages.","oneOf":[{"description":"Error value in error map can be:\n- single string, or\n- array of strings (multiple messages for one field).","type":"string"},{"type":"array","items":{"type":"string"}}]}}},"paths":{"/channels/price-sync":{"get":{"tags":["channels"],"summary":"Price sync rules: how mobile-calendar prices are pushed to OTA portals","description":"Lists the account's price sync rules (panel: Pricing → Price sync). A rule maps ONE source — a mobile-calendar rate plan × room (or room type) — to ONE target: a portal unit (Booking.com room, Airbnb listing, Szállás room) × portal rate plan, with an adjustment (`PERCENT`/`AMOUNT`, signed: +55 = +55 PLN, -10 = -10%) and rounding. While a rule is ACTIVE, every change of the source price in the mobile-calendar pricing (PUT /pricing, bulk changes) is pushed to the portal automatically, after the adjustment and rounding, usually within a minute; a PAUSED rule pushes nothing. A price of 0 in mobile-calendar closes the date on the portal. `lastRun` = the last recalculation/push of the rule (panel: \"Last recalculation\") with its status. PREVIEW: pass `price` (a mobile-calendar price) — every returned rule gets `preview.channelPrice`, the exact amount that would be sent to the portal (same calculation as the sync worker). Use it to answer \"what will the price be on Airbnb/Booking if I set X\": filter by `roomId` (or `typeId`) and `rateId` of the mobile-calendar price being changed. No rule for a unit = changing the mobile-calendar price does not change the portal price. Required scope: `channels.read`.","operationId":"getPriceSyncRules","parameters":[{"name":"channel","in":"query","description":"Only rules of this portal","required":false,"schema":{"type":"string","enum":["booking","airbnb","szallas"]}},{"name":"rateId","in":"query","description":"Only rules whose SOURCE is this mobile-calendar rate plan (GET /rates)","required":false,"schema":{"type":"integer","minimum":1}},{"name":"roomId","in":"query","description":"Only rules whose SOURCE prices this mobile-calendar room (a room inside a room type matches rules of its type — rooms in a type share the type's pricing)","required":false,"schema":{"type":"integer","minimum":1}},{"name":"typeId","in":"query","description":"Only rules whose SOURCE is this room type","required":false,"schema":{"type":"integer","minimum":1}},{"name":"price","in":"query","description":"Preview: a mobile-calendar price (per night, account currency) — each rule gets `preview` with the resulting portal price","required":false,"schema":{"type":"number","minimum":0}}],"responses":{"200":{"description":"Price sync rules (empty list = no price is synced to any portal)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceSyncRulesResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem401"}}}},"403":{"description":"Insufficient scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem403"}}}},"422":{"description":"Validation error — unknown channel, invalid id or price","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiProblem422"}}}}}}}}}
```
