# 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 a [changelog](/developers/guides/changelog) entry. | 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` | Only present when the error carries structure: `errors[]`, `current_offer` or `items[]`. | Yes, for the codes below. |

The rule to keep: **if the status is not 2xx, nothing was booked.** The cases where you cannot know the result are a timeout on your side, and a `502` whose `message` says the booking is being verified. Then repeat the request with the same `external_reference`, or read the booking back with `GET /api/v2/bookings?external_reference=...`.

## 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 | `CREDENTIAL_MISMATCH` | The credential does not match the company sent in the request. | See [Authentication](/developers/guides/authentication). |
| 403 | `FORBIDDEN` | The credential has no access to this product. | 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 id or the reference. |
| 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`, its stored search is gone, or the source no longer has the rate. | Ask availability again. |
| 409 | `PRICE_CHANGED` | The live 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 reference is still running, or its result at the provider is unknown and is being verified (the `message` says which). | Retry the same reference in a few seconds. If the message says it is being verified, do not book again: read `GET /api/v2/bookings?external_reference=...` until the booking appears. |
| 409 | `VOUCHER_NOT_READY` | The voucher is not published yet. | Retry later. |
| 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 passed, or, for a hotel, there is no reservation we can cancel. | 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 upstream did not answer. Nothing was booked and nothing was charged. The exception: when the `message` says the booking is being verified, the result is unknown. | Retry with the same `external_reference`. If it is being verified, the retry answers `409 BOOKING_IN_PROGRESS`: wait and read the booking by reference. |
| 503 | `PROVIDER_UNAVAILABLE` | The product is switched off. 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`), `code` and `message`. The `code` is one of `REQUIRED`, `INVALID_FORMAT`, `INVALID_COUNTRY`, `AGE_NO_CATEGORY`, `AGE_ROLE_MISMATCH`, `OCCUPANCY_MISMATCH`, `DOB_INVALID`, `DATE_NOT_BOOKABLE` (the date is before the minimum lead time of your account, for example a hotel check-in that is too close) and `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).

## 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`, retry with the same `external_reference`. Never use a new one: it can create a second booking.
- A confirmed reference 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`.
- When the provider did not answer and we cannot know whether it booked, the first attempt ends with `502 PROVIDER_ERROR` and the reference stays tied to it. A retry gets `409 BOOKING_IN_PROGRESS`, and so does `GET /bookings?external_reference=...` until the booking is confirmed. Do not book again with a new reference.
- 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`.
