# Hotels

This guide covers what is specific to hotels: destinations and content, an asynchronous search, offers per room, the Disney resort hotels, a booking of several rooms and the hotel confirmation number. The steps every product shares (the offer, the check, booking, retries, reading, cancelling, the voucher and the errors) are in the [Booking flow](/developers/guides/v2-booking-flow) guide. Field-by-field detail is in the [Hotels reference](/developers/reference/v2/hotels) and the [Booking flow reference](/developers/reference/v2/bookings).

| Step | Call | What carries to the next step |
|---|---|---|
| 1 | `GET /api/v2/hotels/destinations` | `destination_id` |
| 2 | `POST /api/v2/hotels/availability`, then `GET /api/v2/hotels/availability/{search_id}` | `offer_id` for each room |
| 3 (optional) | `POST /api/v2/offers/check` | A new `offer_id`, the final policy |
| 4 | `POST /api/v2/bookings` | `booking_id` |
| 5 | `GET /api/v2/bookings/{booking_id}`, then the voucher | The hotel confirmation number |

Content for your own pages is `GET /api/v2/hotels/{hotel_id}`.

## 1. Destinations and content

```bash
curl -G "https://highstartravel.com/api/v2/hotels/destinations" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  --data-urlencode "q=orlando"
```

Each row has our `destination_id`. `kind` says what it is: a city or area (`destination`), a group you can search at once (`hotel_group`), or one hotel (`hotel`, which also has a `hotel_id`). Send the ids in `destination_ids` when you search. A destination your credential cannot sell is not listed.

- With `q` (at least 2 characters) you get a typeahead. Names match in Spanish, English and Portuguese, and misspellings are tolerated. `matched_alias` tells you which name matched.
- Without `q` the list is paginated with `page` and `limit` (up to 500), and you can filter by `kind` and `country`.
- The same place can appear more than once when it is sold from more than one source. We do not merge those rows yet, and each one has its own `hotel_id`.
- Sync the list on a schedule and cache it. The answer has `Cache-Control: private, max-age=3600`.

The content of a hotel is `GET /api/v2/hotels/{hotel_id}`. The basic fields always come. Ask for the heavy parts with `sections`:

```bash
curl -G "https://highstartravel.com/api/v2/hotels/4821" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  --data-urlencode "sections=images,facilities"
```

`sections` is a comma-separated list of `descriptions`, `facilities`, `images` and `contacts`. With `room_id` (the one of an offer) the sections `descriptions`, `facilities` and `images` are about that room. That answer is fetched live and is not cached. If it cannot be fetched now you get `503 PROVIDER_UNAVAILABLE` with `Retry-After`. A hotel we do not sell and a hotel with no content both answer `404 PRODUCT_NOT_FOUND`. Some hotels have a reduced document, so do not assume a section is full.

There is no call that lists every hotel. You learn a `hotel_id` from a search, or from a `kind: hotel` row of the destinations.

## 2. Search

A search can take 10 to 30 seconds, because it asks several sources. It is asynchronous:

1. `POST /api/v2/hotels/availability` starts it and answers with a `search_id` and what is ready after about a second.
2. `GET /api/v2/hotels/availability/{search_id}` returns the same search with more hotels. Repeat every `poll_after_seconds` until `status` is `completed`.

```bash
curl -X POST "https://highstartravel.com/api/v2/hotels/availability" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Accept-Encoding: gzip" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_ids": [5120],
    "check_in": "2026-11-10",
    "check_out": "2026-11-13",
    "rooms": [
      { "adults": 2 },
      { "adults": 2, "children_ages": [7] }
    ]
  }'
```

- `rooms` has one object per room. `adults` goes from 1 to 20. `children_ages` lists the age of every child and infant, 0 to 17. Declare all of them: hotels that do not accept minors are left out when the list is not empty.
- `check_out` must be after `check_in`. A check-in that is too close answers `400 VALIDATION_ERROR` with the issue code `DATE_NOT_BOOKABLE`. The `message` has the earliest date. The minimum is a setting of your account, so read it from the `message`.
- A search takes up to 5 destinations and up to 7 rooms by default. More answers `400 INVALID_FORMAT` on `destination_ids` or `rooms`. These limits can change, and the [changelog](/developers/guides/changelog) announces it.
- Send `Accept-Encoding: gzip`. A full answer can reach several MB.

### Polling

Each answer is everything accumulated so far, not a delta. You can drop the previous answer, and a poll you lost breaks nothing.

- One rule: the `POST` starts the search and answers at once with what is ready. Then repeat `GET /hotels/availability/{search_id}` every `poll_after_seconds` (2 seconds) while `status` is `running`, until `status` is `completed`. Every call answers at once; the server never holds a call waiting for the search.
- `poll_after_seconds` is `null` when the search is `completed`.
- `cached: true` means the answer comes from a recent identical search.
- `partial: true` is set when the search finished and some of the sources did not answer. What you have is valid but not complete. Search again later if coverage matters. When no source answered at all and nothing was found, the answer is `502 PROVIDER_ERROR`.
- The poll takes no body. The rooms, dates and destinations are the ones of the search. It does not start new work at the sources.
- An unknown `search_id`, an expired one, a badly formed one and one that belongs to another company answer the same `404 SEARCH_NOT_FOUND`. Start a new search. The restrictions of your account are applied again on every read, so a destination you can no longer sell answers `403 CATALOG_RESTRICTED`.

### Pages

A search can return many hotels. Send `offset` and `limit` (default 100, at most 500) in the body of the `POST` or in the query of the `GET`. The answer has `total`, `offset`, `limit` and `next_offset`, which is where the next page starts and is `null` on the last page. The order only grows at the end while the search runs, so a page you already read does not move.

### Read the result

```json
{
  "search_id": "EXAMPLE-SEARCH-1",
  "status": "completed",
  "poll_after_seconds": null,
  "cached": false,
  "partial": false,
  "total": 1,
  "offset": 0,
  "limit": 100,
  "next_offset": null,
  "hotels": [
    {
      "hotel_id": 4821,
      "name": "Palm Harbor Resort",
      "adults_only": false,
      "adults_min_age": null,
      "slots": [
        {
          "slot": 1,
          "occupancy": { "adults": 2, "children_ages": [] },
          "offers": [
            {
              "offer_id": "eyJ2IjoxLCJzIjoiYSJ9.EXAMPLE-SIGNATURE-1",
              "stage": "availability",
              "slot": 1,
              "room": { "room_id": "EXAMPLE-ROOM-A", "name": "Standard Room, 2 Queen Beds" },
              "board": { "code": "room_only", "name": "Room only" },
              "currency": "USD",
              "total": 645.0,
              "rate_group": "g-1",
              "rate_total": 1290.0,
              "requires_check": false,
              "expires_at": null
            }
          ]
        }
      ]
    }
  ],
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

The offer is shortened here. The reference has every field.

## 3. Offers, slots and rate groups

- **One slot per room.** `slots[]` has one entry per room you asked for, in your order. Each slot has its own `offers[]`. You book one offer per slot. A hotel that cannot cover every room is not in the answer, so `offers` is never empty.
- **`total` is the price of that room** for the whole stay, with your pricing. The price of a booking is the sum of the `total` of the offers you book. Never multiply it by the number of rooms.
- **Rate groups.** When a source sells several identical rooms as one rate, that rate shows up in each of its slots with the same `rate_group`, and you book all of those offers together. `rate_total` is the price of the whole rate. It is informative: do not add it to `total`. `rate_group` is `null` for a room sold alone.
- **Board.** `board.code` is `room_only`, `bed_and_breakfast`, `half_board`, `full_board`, `all_inclusive` or `other`. `board.name` always has the text of the hotel, so read it when the code is `other`.
- **Hotel fees.** `hotel_fees[]` lists charges the guest pays at the hotel, such as a resort fee or a city tax. They are not in `total` and we do not collect them. Always show them. `amount` can be `null`, and `basis` is `unknown` when the source does not say what the amount is charged on. A fee with an `amount` of `null` still means a fee exists.
- **Cancellation policy.** `cancellation_policy` has `refundable`, `free_until` and `penalties[]`, each with the `from` instant and the `amount` for that room. Dates are UTC and already adjusted to your account, so `free_until` can fall earlier than the date the hotel reports. In the search result of an offer with `requires_check: true` the policy is provisional, and the final one comes with the check.
- **Adults-only.** `adults_only` and `adults_min_age` (`null` means treat it as 18) are in the search and in the content. Show them.
- `remarks` is a list of plain-text notes of the rate. `promo` and `package` are text or `null`.
- **What we leave out.** Hotels that do not accept the minors you declared, hotels we do not sell, rates already inside their penalty window, and hotels that cannot cover every room.

### How long an offer lasts

A search offer has `expires_at: null`. That does not mean valid forever. The offer works while its stored search exists, and the search is deleted after about a day. After that, the check and the booking answer `409 OFFER_EXPIRED` and you search again. The source can also drop the rate earlier, with the same answer. To know whether the price still holds, call the check. The booking always re-quotes, and answers `409 PRICE_CHANGED` (with the current offer), `409 NO_AVAILABILITY` or `409 OFFER_EXPIRED`. The Disney resort hotels are the exception: their check returns a real `expires_at` from the source. See [Offers with requires_check](#offers-with-requires_check-true).

### What to collect

Every offer has `booking_requirements.travelers`. Hotels do not ask for the same data, and the same hotel can be sold from more than one source, so the requirements are on the offer and not on the hotel. Build the traveler form after the guest picks an offer.

- `mode: lead_only`: one holder for the whole booking. The fields are listed in `fields`, and they apply to the `lead`.
- `mode: all_travelers`: every guest of the room, named. `fields` says which ones apply to which guest (`applies_to` is `lead`, `adult`, `child` or `all`). Children send their `age`, and it must be one of the `children_ages` of the room, each one used once.

`age_categories` tells who is a child (0 to 17) and who is an adult (18 and older). Prices do not depend on age (`priced` is `false`): hotels charge per room.

## 4. The check for hotels

The check is described in the [Booking flow](/developers/guides/v2-booking-flow#2-check-the-offer-optional). For hotels it asks the source again, live, and returns the offer with `stage: checked` and the final cancellation policy. The original `offer_id` stays valid. Two fields are specific to hotels:

- `alternatives`: other packages of the same room, each one a complete offer that is already checked and bookable as it is. It is empty when the hotel has none.
- `group_offers`: when the offer has a `rate_group`, the checked offers of the other rooms of the same rate. The rate is checked once for the group, and you book all of them together.

A rate that a commercial rule no longer lets us sell answers `409 NO_AVAILABILITY`. The `message` says why, in our words.

### Offers with `requires_check: true`

Some hotels, such as the Disney resort hotels, work differently. A search for them takes exactly one room, cannot be mixed with other destinations (`400 UNSUPPORTED_COMBINATION`), and answers `completed` on the first call. Their offers carry `requires_check: true`: the rate in the search cannot be reserved as it is.

The check returns the offer with a real `expires_at` from the source. Book before that time. Each ticket package of the room comes in `alternatives` as its own offer, with its own price and cancellation terms. Let the guest choose one and book its `offer_id`.

You can also book the search offer directly. The booking runs the check for you and books what it returns. If that moves the price, you get `409 PRICE_CHANGED` with the current offer, and nothing is booked. Call the check yourself when you want to show the final policy or let the guest choose among the `alternatives` before booking. A credential that is not enabled for these hotels sees them as if they did not exist.

## 5. The hotel item of a booking

The booking is the one described in the [Booking flow](/developers/guides/v2-booking-flow#3-book). A hotel booking has one item per room:

```json
{
  "external_reference": "AG-2026-000871",
  "items": [
    {
      "offer_id": "OFFER_ID_OF_SLOT_1",
      "travelers": [
        { "role": "lead", "first_name": "Ana", "last_name": "Perez", "phone": "+5491144445555", "nationality": "AR" }
      ]
    },
    { "offer_id": "OFFER_ID_OF_SLOT_2", "travelers": [] }
  ]
}
```

Send one item per room, with the `offer_id` and the guests. The price, the stay and the occupancy come from the offer. The rules of a booking of several rooms:

- All the items are of the same hotel and the same stay. Mixing them answers `400 VALIDATION_ERROR` with the issue code `UNSUPPORTED_COMBINATION` on `items`.
- The offers that share a `rate_group` go together in the same booking. If one is missing you get `UNSUPPORTED_COMBINATION`.
- You can book only some of the rooms of a search, each with its own offer. A room of the search can be used once.
- An offer can carry the holder in only one place. With `mode: lead_only`, the holder goes in the first item, the one with the lowest `slot`, with `role: lead`. Every other item sends `travelers: []`. With `mode: all_travelers`, each item lists the guests of its own room: as many `adult` as `occupancy.adults`, and one `child` per entry of `occupancy.children_ages`, with that `age`.
- A traveler that does not match the room answers `400 VALIDATION_ERROR` with `OCCUPANCY_MISMATCH` on the exact field. All the problems come together in `details.errors[]`.
- If a room cannot be booked, the whole booking is rejected. There are no partial confirmations.
- The price is validated live again for every room. If it moved by 0.01 or more, `details.current_offer` is the current offer of that room, and nothing is booked.

The `201` has the whole booking: one item per room, in the order of the slots, with `total`, `board`, `occupancy`, `hotel_fees` and the `cancellation` of that room. The `total` of the booking is the sum of its items.

```json
{
  "booking": {
    "booking_id": 168400,
    "status": "confirmed",
    "external_reference": "AG-2026-000871",
    "currency": "USD",
    "total": 1290.0,
    "items": [ "..." ],
    "hotel_confirmation": { "status": "pending", "numbers": [] },
    "voucher": {
      "url": "https://highstartravel.com/api/v2/bookings/168400/voucher",
      "status": "ready",
      "hcn": { "url": "https://highstartravel.com/api/v2/bookings/168400/voucher?variant=hcn", "status": "pending" }
    },
    "test_mode": false
  },
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

In the items, `travelers` lists the guests as they were stored, without phone numbers. For an offer that needs one holder, the holder appears in every line with `role: lead`, because every room has it. The `slot` of a line is the room of the booking it is.

When the source does not answer and we cannot know whether it booked, the same rule as for every product applies: the reference stays tied to the attempt, a retry answers `409 BOOKING_IN_PROGRESS`, and you read the booking by reference. See [If the result is unknown](/developers/guides/v2-booking-flow#if-the-result-is-unknown).

## 6. Hotel confirmation number and voucher

The hotel confirmation number (HCN) is the number the hotel itself gives to the room. The guest shows it at the front desk. It is not the `booking_id`.

- `hotel_confirmation.status` is `pending` when you book. It becomes `available` later, when at least one room has a number. `numbers[]` has one entry per room that is not cancelled, with `number: null` for a room whose number has not arrived. `hotel_confirmation` is `null` when every room is cancelled.
- Ask when you need it. It changes at most a couple of times in the life of a booking, so do not poll it.
- Until the numbers are delivered to you, the number of a room can change if we rebook the room on our side. The first `GET` that returns numbers, or the download of the `hcn` voucher, records that you received them. From then on they do not change. A read that returns `pending` changes nothing.
- `GET /api/v2/bookings/{booking_id}/voucher` is the voucher without the number. `?variant=hcn` is the voucher with it: hand that one to the guest once `voucher.hcn.status` is `ready`. While `hotel_confirmation.status` is `pending`, it answers `409 VOUCHER_NOT_READY`. Any other `variant` answers `400 VALIDATION_ERROR` on `variant`.
- Voucher delivery is a setting of your account. When it is off, `voucher.status` and `voucher.hcn.status` are `none`, the downloads answer `409 VOUCHER_NOT_READY` and the booking is still valid.
- A cancelled booking answers `410 BOOKING_CANCELLED` for both variants.

## 7. Cancelling a hotel booking

The whole booking is cancelled, never one room. The penalty is the one in the `cancellation` of each room: `refundable`, `free_until` and `penalties[]` show what applies and from when. Cancelling after `free_until` is allowed, and the penalty of the room applies. The answers are the ones of the [Booking flow](/developers/guides/v2-booking-flow#cancel). With `409 CANCELLATION_PARTIAL`, `details.items[]` has the state of each room, and a room in `error` needs attention.

## Test credentials

A test credential runs the search and the content on the real catalog and real prices, and the search answers `test_mode: true`. The booking runs every validation for real but creates no reservation. See [Sandbox](/developers/guides/sandbox).

## What not to do

- Do not assume an offer lives forever because `expires_at` is `null`. A hotel offer works while its search exists, about a day.
- Do not multiply `total` by the number of rooms, and do not add `rate_total` to it.
- Do not leave a child out of `children_ages`. Declare every child and infant.
- Do not hide `hotel_fees`. The guest pays them at the hotel.
- Do not retry a timed-out booking with a new `external_reference`.
- Do not poll the hotel confirmation number. Ask when you need it.

## Next

- The [Booking flow](/developers/guides/v2-booking-flow) guide has the shared steps.
- [Hotels reference](/developers/reference/v2/hotels) and [Booking flow reference](/developers/reference/v2/bookings) have every field and response.
- [v2 errors](/developers/guides/v2-errors) explains every code.
- [Migrating from v1](/developers/guides/v2-migrating-from-v1#hotels-v1-to-v2) maps the v1 hotel calls to these.
