# Booking flow

Every product in API v2 is sold with the same steps. Only the way you get the catalog and the availability changes. This guide describes the shared steps. It uses a transfer as the example item. What is specific to each product is in [Transfers](/developers/guides/v2-transfers), [Tickets](/developers/guides/v2-tickets) and [Hotels](/developers/guides/v2-hotels). For the idea behind the flow, read the [API v2 overview](/developers/guides/v2-overview). Field-by-field detail is in the [Booking flow reference](/developers/reference/v2/bookings).

| Step | Call | What carries to the next step |
|---|---|---|
| 1 | The catalog and availability calls of the product | `offer_id` |
| 2 (optional) | `POST /api/v2/offers/check` | A new `offer_id` and the cancellation policy |
| 3 | `POST /api/v2/bookings` with an `Idempotency-Key` header | `booking_id` |
| 4 | `GET`, `cancel`, `voucher` on `/api/v2/bookings/{booking_id}` | |

For each sale the smallest integration is three calls: the catalog (once, it changes rarely), availability, and the booking. The rest is optional: the check, and reading, cancelling and the voucher after the sale. The next sections use one transfer from the first call to the voucher, with the same `offer_id` and the same `booking_id` in every example.

## 1. The offer

Availability answers with an `offer_id` for each thing you can sell: a transfer day, a ticket day or time slot, a hotel room. The offer is signed. It carries the product, the date, the quantities and the price. You never send those again: the booking takes the `offer_id` and nothing else about the product.

- An `offer_id` is valid for your credential only. An offer that is malformed, altered or issued to another company answers `400 OFFER_INVALID`.
- Most offers have `expires_at: null`. That means no expiry time of its own, not valid forever. A hotel offer is good for a short time after the search: book soon and do not keep it for later. When it is too old, the check and the booking answer `OFFER_EXPIRED` and you search again. The rate can also stop being available earlier, with the same answer.
- To know whether the price still holds, call the check. The booking always checks the price again too.
- Some offers carry `expires_at`. Today that is the Disney resort hotels, after the check. It is the time the offer ends, not a deadline: after it you can still book that `offer_id`, and the booking checks the room again.
- The price you saw is not a promise until the booking. See [Book](#3-book) for what happens when it moved.

Availability for a transfer of 2 adults and 1 child:

```bash
curl -X POST "https://api.avantetravel.com/api/v2/transfers/availability" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{ "product_ids": [301], "from": "2026-11-17", "to": "2026-11-17", "pax": { "adult": 2, "child": 1, "infant": 0 } }'
```

```json
{
  "results": [
    {
      "product_id": 301,
      "currency": "USD",
      "days": [
        { "date": "2026-11-17", "availability": "available", "total": 120.0, "offer_id": "OFFER_ID", "expires_at": null }
      ]
    }
  ],
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

`OFFER_ID` is the value you carry to the next steps. In the examples below it stands for the long signed string that availability returned.

## 2. Check the offer (optional)

```bash
curl -X POST "https://api.avantetravel.com/api/v2/offers/check" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{"offer_id": "OFFER_ID"}'
```

The `offer_id` goes in the JSON body, not in the URL, like in `POST /bookings`. Without it the call answers `400 VALIDATION_ERROR` with `offer_id` in `details.errors[]`. The call quotes the offer again and returns it with `cancellation_policy` and `conditions`. The check returns a new offer (`stage: "checked"`). The original `offer_id` stays valid, and you can book with either.

No product needs the check. Call it when you want the current price or the final cancellation policy before you show the offer to your customer. You can book a Disney resort hotel offer from the search directly: the booking runs the check for you. Call it yourself to show the final policy or the ticket packages of the room first. See [Hotels](/developers/guides/v2-hotels).

- `alternatives` holds other offers for the same item, each one complete and already checked. It is always empty for transfers and tickets.
- If the price moved, the call answers `409 PRICE_CHANGED` with the current offer in `details.current_offer`.
- `409 OFFER_EXPIRED` means the rate is no longer available. `409 NO_AVAILABILITY` means it cannot be sold any more. In both cases ask availability again.

## 3. Book

```bash
curl -X POST "https://api.avantetravel.com/api/v2/bookings" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6c1f0a52-8f0e-4d0b-9a63-2b1e7c3d9a41" \
  -d '{
    "external_reference": "MY-SYSTEM-9876",
    "items": [
      {
        "offer_id": "OFFER_ID",
        "travelers": [
          { "first_name": "John", "last_name": "Doe", "phone": "+15555550123", "nationality": "US" }
        ],
        "logistics": {
          "airport": "MCO",
          "airline": "AA1234",
          "flight_time": "13:45",
          "hotel": "Hotel name"
        }
      }
    ]
  }'
```

- `Idempotency-Key` is required. Use a new value for each booking, for example a UUID, and keep it up to 191 bytes. It is what makes the request safe to repeat. See [If the request times out](#if-the-request-times-out).
- `external_reference` is optional: your own number for this booking, up to 50 bytes (ASCII recommended). It is only data. You can send the same one on several bookings, for example your order number when one order becomes several bookings. It does not protect against duplicates, and there is no call to search bookings by it. Keep the `booking_id` from the answer next to your own id.
- Each item has the `offer_id` and the data of the product: `travelers`, and depending on the product `logistics` or `options`. The product catalog or the offer lists what to send in `booking_requirements` (for tickets, the brand lists it in `GET /tickets/brands`, and the product adds `booking_options`). Build your form from it. The API asks only for what that product needs. A key the product does not list is rejected or ignored, as the product guide says.
- `travelers` are the people. The quantities (`pax`) come from the offer, and you do not send them again.
- `nationality` is an ISO 3166-1 alpha-2 code; alpha-3 codes and country names are accepted and converted. `phone` is in international format.
- A booking takes the items the product allows. Items of different products in one booking answer `400 VALIDATION_ERROR` with `UNSUPPORTED_COMBINATION`.
- A booking that cannot be made in full is rejected as a whole. If the answer is not 2xx, nothing was booked. If it is `202`, the booking is pending: see [If the result is unknown](#if-the-result-is-unknown).

### The price is checked again

The booking always checks the price of the offer again. If something changed, you get one of these:

| Answer | Meaning | What to do |
|---|---|---|
| `409 PRICE_CHANGED` | The current price differs from the offer by 0.01 or more. `details.current_offer` is the current offer. Nothing was created. | Show the new price and book again with the `offer_id` of `details.current_offer`. |
| `409 NO_AVAILABILITY` | It can no longer be sold. | Ask availability again. |
| `409 OFFER_EXPIRED` | The rate is no longer available. | Search again. |

### Validation errors

A validation failure answers `400 VALIDATION_ERROR` and lists every problem at once in `details.errors[]`. Each entry has the `field` path, for example `items[0].travelers[1].birth_date`, and a `code` such as `REQUIRED`. Fix them all and send again.

### The answer

A `201` returns the booking:

```json
{
  "booking": {
    "booking_id": 245117,
    "status": "confirmed",
    "external_reference": "MY-SYSTEM-9876",
    "created_at": "2026-09-30T17:58:12Z",
    "currency": "USD",
    "total": 120.0,
    "items": [
      {
        "item_id": 1,
        "product": { "product_type": "transfer", "product_id": 301, "name": "MCO to Disney Area" },
        "date": "2026-11-17",
        "time_slot": null,
        "pax": { "adult": 2, "child": 1, "infant": 0 },
        "total": 120.0,
        "status": "confirmed",
        "cancellation": { "allowed": true, "deadline": "2026-11-14T18:45:00Z", "penalty": null },
        "lead_traveler": { "first_name": "John", "last_name": "Doe", "phone": "+15555550123", "nationality": "US" },
        "logistics": { "airport": "MCO", "airline": "AA1234", "flight_time": "13:45", "hotel": "Hotel name" }
      }
    ],
    "voucher": { "url": "https://api.avantetravel.com/api/v2/bookings/245117/voucher", "status": "ready" },
    "test_mode": false
  },
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

Keep `booking_id`: reading, cancelling and the voucher use it. The `status` of a booking is one of `confirmed`, `pending`, `failed`, `cancelled`, `partially_cancelled` or `error` (an item that needs attention). An item is shaped by its product type, so ignore the shapes and fields you do not know.

### If the request times out

Repeat the same request with the same `Idempotency-Key` and the same body. Never use a new key for the same booking: it can create a second one.

| What you get | What it means | What to do |
|---|---|---|
| `200` with `idempotent_replay: true` | The first attempt finished. This is the original booking. Nothing new was booked, even if the offer expired or the price moved since. | Use the booking. |
| `409 BOOKING_IN_PROGRESS` | The first attempt is still running. | Wait 5 seconds and repeat the same request. Keep trying for up to 15 minutes. |
| `202` with `booking.status: "pending"` | The first attempt ended with an unknown result. See below. | Read the booking, do not book again. |
| `409 IDEMPOTENCY_KEY_REUSED` | The key was used before with a different body. | Use a new key only if this is a new booking. |
| A `4xx`, or a `5xx` without a booking | Nothing was booked and the key is free again. | Fix the request if it is a `4xx`. Repeat it with the same key. |

A key belongs to your company and is tied to the body of the request, including `external_reference`. A key that has been in progress for more than 15 minutes is taken as abandoned, and a repeat starts the booking again.

### If the result is unknown

When the result of the booking is unknown, `POST /bookings` answers `202`:

```json
{
  "booking": {
    "booking_id": 245117,
    "status": "pending",
    "external_reference": "MY-SYSTEM-9876",
    "created_at": "2026-09-30T17:58:12Z",
    "currency": null,
    "total": null,
    "items": [],
    "voucher": { "status": "none" },
    "test_mode": false
  },
  "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c"
}
```

The booking exists and has an id, but we do not know its result yet. Read it:

```bash
curl "https://api.avantetravel.com/api/v2/bookings/245117" \
  -H "Authorization: Bearer YOUR_CREDENTIAL"
```

- `confirmed`: it was booked. The answer is the full booking.
- `pending`: still unknown. Read again.
- `failed`: nothing was booked. You can book again, with the same key or a new one.

Read every 10 seconds for the first 2 minutes, then every minute. If it is still `pending` after 1 hour, write to support with the `booking_id` and the `request_id`. Do not book it again before it is `failed`: it can create a second reservation. Repeating the `POST` with the same key answers the same `202` until the booking settles. A pending booking cannot be cancelled or have a voucher until it is `confirmed`.

The golden rule: **a response that is not 2xx means nothing was booked. A `202` means pending: read it.**

### Client timeouts

| Call | Set your timeout to |
|---|---|
| Catalog, reads of a booking | 30 seconds |
| Availability and `POST /offers/check` | 60 seconds |
| Hotel search: the start (`POST /hotels/availability`) | 90 seconds |
| Hotel search: each poll | 30 seconds |
| `POST /bookings` and the cancel | at least 400 seconds |
| Voucher | 60 seconds |

A ticket booking or cancellation can take several minutes. If your own timeout is shorter, you will get a timeout on a booking that is still running. Repeat the request with the same `Idempotency-Key`: you get `409 BOOKING_IN_PROGRESS` while it runs, then the result. Never book again with a new key.

## 4. Read, cancel, voucher

### Read

```bash
curl "https://api.avantetravel.com/api/v2/bookings/245117" \
  -H "Authorization: Bearer YOUR_CREDENTIAL"
```

Reading returns the same shape as the booking answer, with the current `status` (`confirmed`, `pending`, `failed`, `cancelled`, `partially_cancelled` or `error`). Do not cache it. There is no search by `external_reference`: keep the `booking_id`.

### Cancel

```bash
curl -X POST "https://api.avantetravel.com/api/v2/bookings/245117/cancel" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer changed plans" }'
```

The call cancels the whole booking. The body is optional, and `reason` is a short note. A body that is not a JSON object answers `400 VALIDATION_ERROR`. Cancelling an already cancelled booking answers `200` with `already_cancelled: true`, so it is safe to retry. The `cancellation` of each item tells what applies: `allowed`, `deadline` and `penalty`. Failures are:

| Answer | Meaning |
|---|---|
| `409 CANCELLATION_REJECTED` | The cancellation was refused. The booking is unchanged. |
| `409 CANCELLATION_PARTIAL` | Some items were cancelled and others were not. `details.items[]` gives the state of each item. An item in `error` needs attention: read the booking and act on it. |
| `409 NOT_CANCELLABLE` | The booking cannot be cancelled through the API: the `cancellation.deadline` of the item passed (transfers), the booking is `pending` or `failed`, or the hotel booking cannot be cancelled online. Nothing was changed. Stop retrying and contact support. |

### Voucher

```bash
curl "https://api.avantetravel.com/api/v2/bookings/245117/voucher" \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -o voucher.pdf
```

`voucher.status` in the booking is one of:

- `ready`: download it now.
- `pending`: it is not published yet. The download answers `409 VOUCHER_NOT_READY`. Retry later. A booking that is itself `pending` answers the same.
- `none`: there is no voucher to download, for example because voucher delivery is off for your account. The download answers `409 VOUCHER_NOT_AVAILABLE`. Do not retry: the booking is still valid. A cancelled booking answers `410 BOOKING_CANCELLED` instead.

The voucher comes in the language set on your credential, and is sent inline with `Cache-Control: private, no-store`. A cancelled booking answers `410 BOOKING_CANCELLED`. The download needs your Bearer credential.

### Your company only

Reading, cancelling and the voucher only see bookings of your company, for the products v2 sells (transfers, tickets, hotels). A booking that does not exist, one that belongs to someone else, and a booking of another product all answer `404 BOOKING_NOT_FOUND`.

## Test credentials

A test credential runs the whole flow on the real catalog and real prices, and every validation runs for real, but no real booking is created. The booking answers `test_mode: true`. Disney makes a real test booking that issues nothing and charges nothing. In every case reading, cancelling and the voucher work on it. `Idempotency-Key` works exactly as in production. See [Sandbox](/developers/guides/sandbox).

## Errors to handle

| Code | Action |
|---|---|
| `VALIDATION_ERROR` | Fix the fields in `details.errors[]`. Check `booking_requirements` of the product (tickets: of the brand). |
| `OFFER_INVALID` | Ask availability again. |
| `PRICE_CHANGED` | Show `details.current_offer` and book again with its `offer_id`. |
| `OFFER_EXPIRED`, `NO_AVAILABILITY` | Ask availability again. |
| `BOOKING_IN_PROGRESS` | Repeat the same request, with the same `Idempotency-Key`, in a few seconds. |
| `IDEMPOTENCY_KEY_REUSED` | The key belongs to a different body. Use a new key for a new booking. |
| `PROVIDER_ERROR`, `PROVIDER_UNAVAILABLE` | Nothing was booked. Retry later with the same `Idempotency-Key`. |
| `CANCELLATION_PARTIAL` | Read the booking and act on the items in `error`. |
| `VOUCHER_NOT_READY` | Retry later. |
| `VOUCHER_NOT_AVAILABLE` | There is no voucher for this booking. Do not retry. |
| `BOOKING_NOT_FOUND` | Check the `booking_id`. |

The full list is in [v2 errors](/developers/guides/v2-errors).
