# Hotels

Hotel search and booking. You search a destination, check the price of a rate, book, and read the booking back.

This guide explains the flow. Field-by-field detail is in the [Hotel reference](/developers/reference/v1/hotel).

| Step | Endpoint | Gives you |
|---|---|---|
| 1 | `GET /api/v1/hotel/content/destinations` | `id_location` to search |
| 2 | `POST /api/v1/hotel/distribution/hotelSearch` | Hotels and a `rate_key` for each rate |
| 3 | `POST /api/v1/hotel/distribution/hotelCheckPrice` | The current price and conditions of a rate |
| 4 | `POST /api/v1/hotel/distribution/bookingConfirm` | `id_orden`, the only identifier of the booking |
| 5 | `GET /api/v1/hotel/distribution/bookingDetail` | The state of the booking |
| 6 | `GET /api/v1/hotel/distribution/bookingHcn` | The hotel's own confirmation number and the final voucher |
| 7 | `POST /api/v1/hotel/distribution/bookingCancelation` | Cancels the booking |

Content endpoints (`content/hotelData`, `content/hotelDetails`, `content/locations`) give you descriptions, facilities and images for your own pages.

All calls need your Bearer credential. Send it only in the header. See [Authentication](/developers/guides/authentication).

## 1. Find a destination

```bash
curl -G "https://api.avantetravel.com/api/v1/hotel/content/destinations" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  --data-urlencode "q=orlando"
```

The `id` of each entry is the `id_location` you pass to `hotelSearch`. Entries of `type: "destination"` are places and entries of `type: "hotel"` are single hotels. With `q` you get type-ahead results. Without `q` you get the whole list, paginated with `page` and `limit` (up to 500).

The list depends on your account. Do not hard-code destination ids. You can cache the response for an hour.

`content/locations` returns the raw tree of countries, divisions and destinations, for partners who sync the geography once a day. Every id in it is ours. To drill down, pass the `id` of a country row as `country_id`, or the `id` of a division row as `division_id`. The `country_id` and `division_id` of each row hold those same ids.

## 2. Search

```bash
curl -X POST "https://api.avantetravel.com/api/v1/hotel/distribution/hotelSearch" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{
    "from_date": "2026-11-15",
    "to_date": "2026-11-20",
    "id_location": 1234,
    "occupancies": [
      { "adults": 2, "childrenAges": [10, 8] },
      { "adults": 2 }
    ]
  }'
```

- `from_date` is the check-in and `to_date` the check-out. The check-out must be after the check-in.
- There is one object in `occupancies` for each room. `adults` goes from 1 to 20.
- `childrenAges` is a list of ages, from 0 to 17, not a count. Declare every child, babies included.
- A check-in that is too close returns `400 Date not bookable`. The `message` has the earliest date. The minimum can change, so read it from the `message` and do not hard-code it.

### Two ways to receive results

A search can take a while. The default mode, `paged`, gives you results as they arrive:

1. The first call starts the search and returns what is ready after a second or two, with `finish: false` and an `id_search`.
2. Repeat the same call with the `id_search` added, every 1 to 3 seconds. Each answer has all the hotels found so far.
3. Stop when you get `finish: true`.

An `id_search` that does not exist or has expired returns `400 Unknown or expired id_search`. Start a new search.

The other mode, `"mode": "full"`, waits on the server until the search ends and returns everything in one answer. If the wait runs out, you get what was found with `finish: false` and an `id_search` to continue in `paged` mode. Use `paged` for a screen a person is waiting at, and `full` for batch jobs. A search typically finishes in 10 to 30 seconds, but a `full` search can wait up to about three minutes. Use a client timeout of 180 seconds for `full`.

`isCached` is informational. You can ignore it. Send `Accept-Encoding: gzip`. Responses can be several megabytes.

### Read the result

Each hotel has one entry in `occupancies` per room you asked for, in the same order. Choose one rate in each entry.

```json
{
  "hotels": [
    {
      "id_hotel": 5821,
      "hotel_name": "Example Hotel",
      "adults_only": false,
      "occupancies": [
        {
          "room": 1, "adults": 2, "childrenAges": [10, 8],
          "rates": [
            {
              "id_room": "123",
              "room_name": "Standard Room, 2 Queen Beds",
              "board": { "name": "Room Only" },
              "currency": "USD",
              "price": 612.50,
              "price_total": 612.50,
              "room_quantity": 1,
              "non_refundable": false,
              "cancelation_fees": [ { "from": "2026-11-08T00:00:00", "amount": 122.50, "amount_total": 122.50 } ],
              "rate_key": "opaque-string"
            }
          ]
        }
      ]
    }
  ],
  "finish": true,
  "isCached": false,
  "id_search": "18452300731942",
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

- `price` is the price of that room, with your pricing applied. The total of the booking is the sum of the `price` of the rates you choose.
- Hotels that cannot cover every room you asked for are not listed.
- If you ask for several identical rooms, you can get one grouped rate that covers them all. The response still gives you one entry per room. The grouped rate appears in each of its entries with the same `rate_key`. The `price` is already divided per room, and `price_total` has the total of the rate. `room_quantity` tells you how many rooms the rate covers.
- `non_refundable: true` marks a rate that cannot be refunded.

### Adults-only hotels

Some hotels do not accept children. They come with `adults_only: true` and, when known, `adults_min_age`. If your search includes children, these hotels are not returned. Show the restriction on your own pages. Always declare every child in `childrenAges`: the hotel sees them at check-in even if your booking did not list them.

## 3. The rate key

The `rate_key` is an opaque, short-lived string. Do not store it and do not change it. Use it in the same search session. If a later call says the key is expired or invalid, search again.

The field name changes at each step. For most hotels the value is the same string (the Walt Disney World resorts are different, see their section below):

| Where | Field |
|---|---|
| `hotelSearch` response | `rate_key` |
| `hotelCheckPrice` request | `rate_key` |
| `hotelCheckPrice` response | `rateKey` |
| `bookingConfirm` request | `ratekey`, a list |

## 4. Check the price

Call this right before you confirm. It gives the current price and conditions of the rate. A rate that passes here passes the same checks at booking.

```bash
curl -X POST "https://api.avantetravel.com/api/v1/hotel/distribution/hotelCheckPrice" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{ "rate_key": "opaque-string" }'
```

The price to show the customer is in `hotels[0].occupancies[].rooms[].rates[].prices.price` and in `total.price`. For most hotels you confirm with the same `rate_key`. A Walt Disney World resort is confirmed with the key this call returns.

| Status | `error` | Meaning |
|---|---|---|
| 502 | `External API error` | The rate is no longer available, or the key expired. Search again. |
| 400 | `Rate not bookable` | The rate cannot be booked. The `message` says why. Choose another rate. |

## 5. Book

```bash
curl -X POST "https://api.avantetravel.com/api/v1/hotel/distribution/bookingConfirm" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{
    "ratekey": ["opaque-string-room-1", "opaque-string-room-2"],
    "bookingHolder": { "name": "John", "surname": "Doe", "phonePrefix": "+54" },
    "external_reference": "MY-HOTEL-ORDER-123"
  }'
```

- `ratekey` is a list with one entry per room: the key of the rate you chose in each room. If a grouped rate covers several rooms, send its key once for each room. Sending it once is also accepted. A grouped key repeated a different number of times returns `400 Invalid rate_key repetition` and nothing is created.
- `bookingHolder` needs `name` and `surname`.
- Always send `external_reference`. See [Conventions](/developers/guides/conventions#idempotency).

Prices are not held. The call quotes every rate again at booking time. If a rate is gone you get `400 RateKey expired`. If a rate cannot be booked you get `400 Rate not bookable`. The booking is made at the price valid at that moment, which can differ from the search. Call `hotelCheckPrice` first and show the customer that price.

The response has the order id:

```json
{
  "statusCode": "SUCCESS",
  "id_orden": 91783,
  "booking": {
    "status": "CO",
    "checkIn": "2026-11-15",
    "checkOut": "2026-11-20",
    "total": { "price": 1225.00, "currency": "USD" }
  },
  "pdf_voucher": "https://example.com/voucher.pdf",
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

- Keep `id_orden`. It is the only identifier of the booking, and the one you use to cancel, read the detail and ask for the confirmation number.
- `booking.hotels[].rooms[]` has one entry per physical room. `hotelLocator` is always `pending` here. The hotel's number comes from `bookingHcn`.
- `pdf_voucher` is the voucher without the hotel's number. It can be missing.

If the booking is refused, you get `400 Booking rejected`. Nothing was reserved and nothing was charged. The `message` gives the reason in a fixed sentence. If any room of a multi-room booking cannot be booked, the whole booking is refused. A failed call never returns `id_orden`.

If the result of the booking is not known yet, you get `409 ORDER_CONFIRMATION_IN_PROGRESS`. The booking may already exist. Do not book again with a new reference: repeat the same request with the same `external_reference` a few minutes later to get the result. If you sent no `external_reference`, do not repeat the call: contact support with the `request_id`.

If `bookingConfirm` times out, repeat it with the same `external_reference`. If the first booking exists, you get it back with `200` and `"idempotent_replay": true`, in the same shape as the first answer, even if the `rate_key` has expired in the meantime. This does not apply to a credential in test mode. Never repeat with another reference. See [Conventions](/developers/guides/conventions#idempotency).

## 6. Read the booking and the confirmation number

```bash
curl -G "https://api.avantetravel.com/api/v1/hotel/distribution/bookingDetail" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  --data-urlencode "id_orden=91783"
```

`booking.status` is `confirmed`, `cancelled` or `error`. `error` means a cancellation did not complete. Contact support with the `id_orden`. A booking that does not exist, or belongs to another account, returns `404 Booking not found` and nothing tells you which of the two it is.

`bookingHcn` returns the Hotel Confirmation Number, the number the hotel itself gives to the room. `hcn_status` is `pending` until the number is final, `available` when it is, and `cancelled` when every room is cancelled. `rooms[]` has one entry per room, with `hcn: null` for a room whose number has not arrived yet. The `pdf_voucher` that comes with `available` includes the number. Do not poll this call often. The status changes at most a few times in the life of a booking. Ask for it when you need the number for the traveler.

## 7. Cancel

```bash
curl -X POST "https://api.avantetravel.com/api/v1/hotel/distribution/bookingCancelation" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{ "id_orden": 91783 }'
```

The whole booking is cancelled. The cancellation charges are the `cancelation_fees` you saw at search and check-price time.

| Status | `error` | Meaning |
|---|---|---|
| 400 | `No active reservation found.` | No active hotel booking of your account has that `id_orden`, or it is already cancelled. |
| 400 | `Cancellation not allowed` | The booking does not belong to your credential. |
| 400 | `Cancellation rejected` | The cancellation was refused. The booking is still active and unchanged. |

## Walt Disney World resorts

Some accounts can also sell Walt Disney World resorts through the same endpoints. If your account does not have them, asking for one returns `403 Access forbidden`. Ask your account manager to enable them.

The flow is the same, with these differences:

- Find "Walt Disney World Resorts - All Hotels" or a single resort in `content/destinations`.
- A Disney search takes exactly one occupancy, which means one room. It cannot be mixed with another kind of location. It returns all results in the first call, so there is nothing to poll and `finish` is always `true`.
- Each rate can carry `promo`, `package` and `inclusions`, which describe what is included, such as the room and a park ticket.
- The `rate_key` of a Disney rate lives only for a short time. Call `hotelCheckPrice` right after the search, and confirm with the `rate_key` it returns: the search key cannot be confirmed and answers `400 Invalid rate_key`. Its answer has `expires_at`: confirm before that time, or quote again. It also has `ticket` (the ticket already included, or `null`) and `ticket_options[]`: the same room with other ticket packages, each with its own `rate_key`, price and cancellation terms.
- A Disney booking is one `rate_key` for one room. Every guest needs an age, so you send the holder in `bookingHolder` and all the other guests in `guests`:

```json
{
  "ratekey": ["rate-key-from-checkprice-or-ticket-options"],
  "bookingHolder": { "name": "Ana", "surname": "Perez", "email": "ana@example.com", "phone": "+54 9 11 5555-1234", "country": "AR" },
  "guests": [
    { "name": "Juan", "surname": "Perez", "age": 40 },
    { "name": "Leo", "surname": "Perez", "age": 7 }
  ],
  "external_reference": "MY-HOTEL-ORDER-124"
}
```

The number of guests, and how many of them are under 18, must match the occupancy you searched. If not, you get `400 Invalid guests` and the message says what is missing.

If the price of the rate changed since `hotelCheckPrice`, the booking answers `400 Price changed`. Call `hotelCheckPrice` again and confirm with the new `rate_key`.

## Accounts with a limited catalog

An account can be limited to Universal Orlando hotels. If you ask for a location or hotel outside your catalog you get `403 Catalog restricted`. `content/destinations` already returns only what you can sell.

## What not to do

- Do not store a `rate_key` for later. It is short-lived.
- Do not confirm without calling `hotelCheckPrice` first.
- Do not leave a child out of `childrenAges`. Declare every child, babies included.
- Do not retry a timed-out `bookingConfirm` with a new `external_reference`.

## Next

- [Hotel reference](/developers/reference/v1/hotel) has every field and response.
- [Sandbox](/developers/guides/sandbox) explains how to test a booking.
- [Errors](/developers/guides/errors) explains every status code.
