---
title: "Best OTA for a property the user has already chosen"
method: POST
path: "/recommendations/by-hotel"
tags: ["decision"]
---

# Best OTA for a property the user has already chosen

`POST /recommendations/by-hotel`

## Request

- Base URL: `https://api.b2a.bluepillow.com/v1`
- URL: `https://api.b2a.bluepillow.com/v1/recommendations/by-hotel`
- Auth: HTTP bearer

## Headers

- `Idempotency-Key` string, uuid, required
- `B2A-Version` string, date

## Request body

- ByHotelRequest
  - `property_id` string, required
  - `dates` StayDates, required
    - `check_in` string, date, required
    - `check_out` string, date, required
  - `guests` Guests, required
    - `adults` integer, required
    - `children_ages` integer[]
    - `rooms` integer

## Response `200`

Decision wrapping the chosen property with per-OTA offers

- Decision
  - `recommended` Option, required
    - `property` Property, required
      - `id` string, required
      - `name` string, required
      - `brand` string, nullable
      - `chain` string, nullable
      - `property_type` 'hotel' | 'aparthotel' | 'apartment' | 'villa' | 'bnb' | 'hostel' | 'resort', required
      - `rating` number, required
      - `stars` integer, nullable
      - `location` LocationObject, required
        - `lat` number, required
        - `lon` number, required
        - `address` string, required
        - `city` string, required
        - `country` string, required
        - `neighborhood` string, nullable
      - `amenities` string[], required
      - `images` ImageObject[]
        - `url` string, uri, required
        - `caption` string, nullable
      - `web_url` string, nullable: BluePillow property-page URL with the caller's search context (check-in/out, language, currency, party size) baked into query params. Links directly to the BluePillow listing page, not an OTA. None when the search context is too sparse to build a meaningful URL (e.g. single-property lookup without dates).
      - `description` string, nullable: Free-text property description from the canonical AccomodationIndex doc. Populated by the single-property endpoint; null when not available or on the search path.
      - `features` FeatureItem[]: Feature/facility tags from BP's AccomodationFeature list (e.g. non-smoking, wifi, pool). Populated by the single-property endpoint; empty on the search path (search-result payloads do not carry this field).
        - `code` string, required
        - `type` string, nullable
        - `name` string, nullable
      - `reviews_aggregate` ReviewsAggregate: rating_count and comment_count are two DISTINCT magnitudes — do not conflate. rating_count = reviews backing the score (OTA-published aggregate, e.g. 156). comment_count = readable review TEXTS available (the crawled subset, e.g. 53), fetchable via the reviews_sample / reviews_extended includes.
        - `score_0_5` number, required
        - `rating_count` integer, required
        - `comment_count` integer, nullable
        - `distribution_by_ota` object
      - `reviews_sample` ReviewSampleItem[], nullable: Recent review texts. On search, populated by include=reviews_sample (cap 5). On property detail, populated by include=reviews_sample (cap 5) or include=reviews_extended (cap 20). null when not requested; empty when requested but no review texts are available. See reviews_meta for the cap.
        - `score` number, required
        - `description` string, required
        - `language` string, nullable
        - `date` string, date-time, required
        - `connector_name` string, nullable
      - `reviews_meta` ReviewsMeta: Provenance for the reviews_sample list on property detail. Makes explicit that the list is deliberately capped (token efficiency), not a data limit: the aggregate rating already reflects ALL reviews, and the omitted texts are older.
        - `returned` integer, required
        - `total_available` integer, required
        - `capped` boolean, required
        - `cap` integer, required
        - `note` string, required
      - `ota_coverage` OTA[], required
      - `dedup_meta` DedupMeta
        - `source_listings` integer, required
        - `confidence` number, required
        - `last_reconciled` string, date-time, required
      - `offers` Offer[]: Live per-OTA offer list from BluePillow GET /search, sorted best-first. Populated only when the BP bridge is active and dates/guests were provided. Empty when no live data is available (Mongo fallback path).
        - `ota` string, required: Connector name (e.g. 'Booking', 'Agoda') as reported by BluePillow.
        - `amount` number, required
        - `amount_per_night` number, nullable
        - `currency` string, required
        - `breakfast_included` boolean
        - `refundable` boolean
        - `is_discounted` boolean
        - `discount_percentage` number, nullable
        - `deeplink_url` string, nullable: BluePillow skippy redirect URL (tracked redirect to the OTA booking page). Opens the OTA landing page for this property with dates/guests pre-filled, routing through BluePillow's tracking layer. Not a direct OTA link. None when BP did not return a URL for this offer.
        - `rooms_left` integer, nullable: Number of rooms remaining at this rate per BluePillow's live-availability fan-out. A low value (1-3) is a scarcity signal. Populated from /search when BP reports it; None when not available.
      - `price` Price
        - `amount` number, required
        - `amount_per_night` number, nullable
        - `currency` string, required
        - `breakfast_included` boolean
        - `refundable` boolean
        - `is_discounted` boolean
        - `discount_percentage` number, nullable
        - `ota` string, nullable
      - `availability_status` 'available' | 'unavailable' | 'unknown': Live availability state: available = BP confirmed bookable rooms; unavailable = BP reported no rooms for the requested dates; unknown = dates not provided or live fan-out was skipped.
  - `alternatives` Option[]
    - `property` Property, required
      - `id` string, required
      - `name` string, required
      - `brand` string, nullable
      - `chain` string, nullable
      - `property_type` 'hotel' | 'aparthotel' | 'apartment' | 'villa' | 'bnb' | 'hostel' | 'resort', required
      - `rating` number, required
      - `stars` integer, nullable
      - `location` LocationObject, required
        - `lat` number, required
        - `lon` number, required
        - `address` string, required
        - `city` string, required
        - `country` string, required
        - `neighborhood` string, nullable
      - `amenities` string[], required
      - `images` ImageObject[]
        - `url` string, uri, required
        - `caption` string, nullable
      - `web_url` string, nullable: BluePillow property-page URL with the caller's search context (check-in/out, language, currency, party size) baked into query params. Links directly to the BluePillow listing page, not an OTA. None when the search context is too sparse to build a meaningful URL (e.g. single-property lookup without dates).
      - `description` string, nullable: Free-text property description from the canonical AccomodationIndex doc. Populated by the single-property endpoint; null when not available or on the search path.
      - `features` FeatureItem[]: Feature/facility tags from BP's AccomodationFeature list (e.g. non-smoking, wifi, pool). Populated by the single-property endpoint; empty on the search path (search-result payloads do not carry this field).
        - `code` string, required
        - `type` string, nullable
        - `name` string, nullable
      - `reviews_aggregate` ReviewsAggregate: rating_count and comment_count are two DISTINCT magnitudes — do not conflate. rating_count = reviews backing the score (OTA-published aggregate, e.g. 156). comment_count = readable review TEXTS available (the crawled subset, e.g. 53), fetchable via the reviews_sample / reviews_extended includes.
        - `score_0_5` number, required
        - `rating_count` integer, required
        - `comment_count` integer, nullable
        - `distribution_by_ota` object
      - `reviews_sample` ReviewSampleItem[], nullable: Recent review texts. On search, populated by include=reviews_sample (cap 5). On property detail, populated by include=reviews_sample (cap 5) or include=reviews_extended (cap 20). null when not requested; empty when requested but no review texts are available. See reviews_meta for the cap.
        - `score` number, required
        - `description` string, required
        - `language` string, nullable
        - `date` string, date-time, required
        - `connector_name` string, nullable
      - `reviews_meta` ReviewsMeta: Provenance for the reviews_sample list on property detail. Makes explicit that the list is deliberately capped (token efficiency), not a data limit: the aggregate rating already reflects ALL reviews, and the omitted texts are older.
        - `returned` integer, required
        - `total_available` integer, required
        - `capped` boolean, required
        - `cap` integer, required
        - `note` string, required
      - `ota_coverage` OTA[], required
      - `dedup_meta` DedupMeta
        - `source_listings` integer, required
        - `confidence` number, required
        - `last_reconciled` string, date-time, required
      - `offers` Offer[]: Live per-OTA offer list from BluePillow GET /search, sorted best-first. Populated only when the BP bridge is active and dates/guests were provided. Empty when no live data is available (Mongo fallback path).
        - `ota` string, required: Connector name (e.g. 'Booking', 'Agoda') as reported by BluePillow.
        - `amount` number, required
        - `amount_per_night` number, nullable
        - `currency` string, required
        - `breakfast_included` boolean
        - `refundable` boolean
        - `is_discounted` boolean
        - `discount_percentage` number, nullable
        - `deeplink_url` string, nullable: BluePillow skippy redirect URL (tracked redirect to the OTA booking page). Opens the OTA landing page for this property with dates/guests pre-filled, routing through BluePillow's tracking layer. Not a direct OTA link. None when BP did not return a URL for this offer.
        - `rooms_left` integer, nullable: Number of rooms remaining at this rate per BluePillow's live-availability fan-out. A low value (1-3) is a scarcity signal. Populated from /search when BP reports it; None when not available.
      - `price` Price
        - `amount` number, required
        - `amount_per_night` number, nullable
        - `currency` string, required
        - `breakfast_included` boolean
        - `refundable` boolean
        - `is_discounted` boolean
        - `discount_percentage` number, nullable
        - `ota` string, nullable
      - `availability_status` 'available' | 'unavailable' | 'unknown': Live availability state: available = BP confirmed bookable rooms; unavailable = BP reported no rooms for the requested dates; unknown = dates not provided or live fan-out was skipped.
  - `metadata` DecisionMetadata, required
    - `price_as_of` string, date-time, nullable
    - `total_options_considered` integer, required
    - `request_id` string, required

## Other responses

- `400`: Invalid request
- `401`: Auth missing or invalid
- `404`: Property not found
- `429`: Rate limited

---

[API](https://skmtc.dev/bluepillow/apis/b2a-api.md) · [All operations](https://skmtc.dev/bluepillow/apis/b2a-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bluepillow/apis/b2a-api/revisions/1dfd82be92b6?raw)
