# Errors

Every v2 error uses one envelope, on every endpoint:

```json
{
  "error": {
    "code": "PRICE_CHANGED",
    "message": "The price of this offer changed. Review current_offer and book again.",
    "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c",
    "details": {}
  }
}
```

| Field | What it is | Safe to parse? |
|---|---|---|
| `code` | A value from a closed catalog. New codes are never added without prior notice. | Yes. Branch on it. |
| `message` | English text for a person. The wording can change. | No. |
| `request_id` | The id of this request, also in the `X-Request-Id` header. | Keep it and send it to support. |
| `details` | Always present. An empty object `{}` when there is nothing more, otherwise `errors[]`, `current_offer` or `items[]`. | Yes, for the codes below. |

The rule to keep: **if the status is not 2xx, nothing was booked.** Two cases need care. A `202` is not an error: the booking is `pending`, and you read `GET /api/v2/bookings/{booking_id}` until it is `confirmed` or `failed`. A timeout on your side means you do not know the result: repeat the request with the same `Idempotency-Key`.

## Codes

| HTTP | `code` | Meaning | What to do |
|---|---|---|---|
| 400 | `VALIDATION_ERROR` | A field is missing or invalid. `details.errors[]` lists every problem with its field path. | Fix the fields and send again. |
| 400 | `OFFER_INVALID` | The offer is malformed, altered, or was issued to another credential company. The answer is the same for all three. | Ask availability again. |
| 401 | `AUTHENTICATION_REQUIRED` | No `Authorization: Bearer` header. | Send your credential. |
| 403 | `FORBIDDEN` | We do not recognise the credential, or it has no access to this product. | Check the token first. If it is right, ask your account manager. |
| 403 | `CATALOG_RESTRICTED` | The credential cannot sell the requested catalog. | Ask your account manager. |
| 403 | `IP_NOT_ALLOWED` | The request IP is not in the credential's allowed list. | Call from an allowed IP. |
| 404 | `PRODUCT_NOT_FOUND` | Unknown product, or one your credential cannot sell. Both answer the same. | Reload the catalog. |
| 404 | `BOOKING_NOT_FOUND` | No such booking for your company. A booking of someone else answers the same. | Check the `booking_id`. |
| 404 | `SEARCH_NOT_FOUND` | No such search for your company: it never existed, it expired, or it belongs to someone else. All three answer the same. | Start the search again. |
| 404 | `ROUTE_NOT_FOUND` | The path does not exist. | Check the path. |
| 405 | `METHOD_NOT_ALLOWED` | Wrong method for the path. `Allow` lists the valid ones. | Use a listed method. |
| 409 | `OFFER_EXPIRED` | The offer cannot be used any more: it passed its `expires_at`, it is too old (a hotel offer is good for a short time after the search), or the rate is no longer available. | Ask availability again. |
| 409 | `PRICE_CHANGED` | The current price differs from the offer by 0.01 or more. `details.current_offer` is a fresh offer. | Show the new price and book again with its `offer_id`. |
| 409 | `NO_AVAILABILITY` | The product is no longer available for that date. | Ask availability again. |
| 409 | `BOOKING_IN_PROGRESS` | The first attempt with this `Idempotency-Key` is still running. | Wait 5 seconds and repeat the same request with the same key. |
| 409 | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was already used with a different body. | Use a new key for a new booking. Repeat the original body to get the original answer. |
| 409 | `VOUCHER_NOT_READY` | The voucher is not published yet, or the booking is still `pending`. | Retry later. |
| 409 | `VOUCHER_NOT_AVAILABLE` | The booking has no voucher (`voucher.status` is `none`), for example because voucher delivery is off for your account. | Do not retry. The booking is still valid. |
| 409 | `CANCELLATION_REJECTED` | The cancellation was rejected. The booking is unchanged. | Do not assume it is cancelled. |
| 409 | `CANCELLATION_PARTIAL` | Some items were cancelled and some were not. `details.items[]` gives the state of each. | Read the booking and act on the items in error. |
| 409 | `NOT_CANCELLABLE` | The booking cannot be cancelled through the API: the `cancellation.deadline` of a transfer passed, the booking is `pending` or `failed`, or the hotel booking cannot be cancelled online. | Stop retrying: the booking stays as it is. Contact support if you need it cancelled. |
| 410 | `BOOKING_CANCELLED` | The booking was cancelled. The voucher is no longer valid. | Stop asking for that voucher. |
| 429 | `RATE_LIMITED` | Too many requests for this credential. | Wait `Retry-After` seconds. |
| 500 | `INTERNAL_ERROR` | Unexpected failure. Anything started by the request was rolled back. | Retry with backoff. Send the `request_id` if it persists. |
| 502 | `PROVIDER_ERROR` | The booking or price could not be completed. Nothing was booked and nothing was charged. When the result is unknown, `POST /bookings` does not answer this: it answers `202`. | Retry with the same `Idempotency-Key`. |
| 503 | `PROVIDER_UNAVAILABLE` | This product or this content is not available right now. Nothing was booked. | Retry later. |
| 503 | `PRICING_UNAVAILABLE` | Pricing is temporarily unavailable for this product. Nothing was booked. | Retry later. |

### Validation issue codes

Each entry in `details.errors[]` has `field` (for example `items[0].travelers[0].phone`, or `null` when the whole body is wrong), `code` and `message`. The `code` is one of:

| `code` | Meaning |
|---|---|
| `REQUIRED` | A required field or header is missing, for example `Idempotency-Key`. |
| `INVALID_FORMAT` | The value has the wrong type, format or length. |
| `INVALID_COUNTRY` | A country that is not ISO 3166-1 alpha-2 (alpha-3 codes and names are converted when they can be). |
| `DOB_INVALID` | A birth date that is not a valid past date. |
| `AGE_NO_CATEGORY` | The age of the traveler does not match any category of the product. |
| `AGE_ROLE_MISMATCH` | The age of the traveler does not match the declared `role`. |
| `OCCUPANCY_MISMATCH` | The number of travelers (or guests of a room) does not match the offer, or the party is too big. |
| `DATE_NOT_BOOKABLE` | The date is before the minimum lead time of your account, for example a hotel check-in that is too close. |
| `UNSUPPORTED_COMBINATION` | Values that are valid alone but cannot go together: items of different products in one booking, hotel rooms of different hotels or stays, the rooms of one rate that are not all in the booking, or a Disney resort destination searched with other destinations or with more than one room. |
| `INVALID_JSON` | The body is not valid JSON, or it is not a JSON object. `field` is `null`. |
| `NOT_ACCEPTED` | A field that v2 does not take. Today that is `id_company`: the credential goes only in the `Authorization` header. |

## PRICE_CHANGED

`POST /bookings` and `POST /offers/check` both answer it. Nothing was created. The body carries the current offer:

```json
{
  "error": {
    "code": "PRICE_CHANGED",
    "message": "The price of this offer changed. Review current_offer and book again.",
    "request_id": "3f8a9b7c1d2e4f5a6b7c8d9e0f1a2b3c",
    "details": { "current_offer": { "offer_id": "NEW_OFFER_ID", "stage": "checked", "total": 130.0 } }
  }
}
```

The object in `details.current_offer` is an offer, shown above with only some of its fields. Show the new `total` to your customer. If they accept, book again with the new `offer_id`.

## Retries

- After a timeout, a `5xx` or a `PROVIDER_ERROR`, repeat the booking with the same `Idempotency-Key` and the same body. Never use a new key for the same booking: it can create a second one.
- A confirmed key answers `200` with the original booking and `idempotent_replay: true`, even if the offer expired or the price moved since.
- While the first attempt is running, a retry gets `409 BOOKING_IN_PROGRESS`. Wait 5 seconds and repeat.
- The same key with a different body answers `409 IDEMPOTENCY_KEY_REUSED`.
- When the result of a booking is unknown, `POST /bookings` answers `202` with `booking.status: "pending"`. Read `GET /bookings/{booking_id}` until it is `confirmed` or `failed`. Do not book again before it is `failed`. If it is still `pending` after 1 hour, write to support with the `booking_id`.
- A rejection and an error before the booking exists free the key: you can repeat it.
- Cancel is safe to retry: an already cancelled booking answers `200` with `already_cancelled: true`.
- Do not retry a `4xx` without changing the request, apart from `409 BOOKING_IN_PROGRESS`, `409 VOUCHER_NOT_READY` and `429`.
