# Errors

> The error shapes of the API, every error code you can meet, request ids and what to do about each kind of error.

Source: https://fx-port.com/docs/errors

Every failed call answers an HTTP status of 400 or more and a JSON body of the same shape, whichever endpoint it came from. Write one error handler against this page and it works for flights, hotels, eSIMs and the account.

## The error body

~~~json
{
  "environment": "sandbox",
  "success": false,
  "errors": [
    {
      "code": "invalid_airport_code",
      "type": "validation_error",
      "title": "Invalid airport code",
      "message": "origin must be a 3-letter IATA code; got 'ALGG'.",
      "field": "origin",
      "details": {},
      "documentation_url": "https://fx-port.com/docs/errors#invalid_airport_code"
    }
  ],
  "meta": { "request_id": "req_8f3a9c1d2e4b", "status": 400 },
  "request_id": "req_8f3a9c1d2e4b"
}
~~~

| Key | Meaning |
|---|---|
| `environment` | `sandbox` or `live`: the environment of your key. |
| `success` | Always `false` for an error. |
| `errors` | The errors, most important first. **This is what you parse.** |
| `errors[].code` | A stable, machine-readable code in `snake_case`. Branch on this, never on the message. |
| `errors[].type` | The family of the error: it tells you [how to react](#what-to-do-about-each-type). |
| `errors[].title` | A short label. |
| `errors[].message` | A sentence for a human: what is wrong and how to fix it. The wording can change, the code does not. |
| `errors[].field` | The request field the error is about, or `null`. |
| `errors[].details` | Structured extras, or `null`: for example `available`, `required` and `currency` on an insufficient balance. |
| `errors[].documentation_url` | The entry of this code in the [catalogue](#every-error-code) below. |
| `meta`, `request_id` | The id of the request: `meta.request_id` is the `X-Request-Id` header. Quote it when you write to support (see [Request ids](https://fx-port.com/docs/conventions.md#request-ids)). |


> [!NOTE]
> The older keys of each endpoint (`detail`, `error`, `message`, `code`, `requestId` ...) stay in the body next to `errors`, so integrations written before `errors` existed keep working. Code that you write now should read `errors` only. The [older shapes](#older-shapes) are listed below.


## Check `success`, not only the status

A few post-booking endpoints (paying an order, an order quote, a cancellation quote, confirming a cancellation) answer a refusal of the supplier with **HTTP 200 and `"success": false`**. The body then carries the same `errors` array. Treat a response as successful only when the status is 2xx **and** `success` is not `false`.

~~~python
data = response.json()
if not response.ok or data.get("success") is False:
    error = (data.get("errors") or [{}])[0]
    raise ApiError(response.status_code, error.get("code"), error.get("message"), data.get("request_id"))
~~~

## What to do about each type

| `type` | What it means | What to do |
|---|---|---|
| `authentication_error` | The key is missing, malformed, unknown or revoked. | Fix the key or the way it is sent. Retrying the same request fails again. |
| `permission_error` | The key is valid but may not do this: it needs Read+Write permission, an allowed IP address or an approved agency. | Do not retry. Use the right key. |
| `validation_error` | The request is wrong. | Read `errors[].field` and `message`, fix the request, send it again. Do not retry unchanged. |
| `not_found` | The object does not exist for your agency (or the id is wrong). | Do not retry. Check the id and the environment of your key. |
| `conflict` | The request is valid but cannot be done in the current state: already booked, already uploaded, duplicate. | Read the code and `details`. Do not repeat it blindly. |
| `payment_error` | Your agency balance cannot cover the request (`402`, live only). | Top up the balance and repeat the request. `details` has `available`, `required` and `currency`. |
| `rate_limit_error` | Too many requests. | Wait for the seconds in `Retry-After`, then retry with exponential backoff and jitter. See [Rate limits](https://fx-port.com/docs/rate-limits.md). |
| `supplier_error` | The airline, hotel or eSIM supplier refused or could not answer. `code` is then often the supplier's own code (for example `4926` or `validation_type`), and `details.supplier_details` has what the supplier said. | Some are permanent (fare gone, sold out, invalid passenger data): fix the request or search again. Some are temporary: retry once or twice with backoff. |
| `internal_error` | Something failed on our side. | Retry once after a short wait. If it persists, write to [dev@fx-port.com](mailto:dev@fx-port.com) with the `request_id`. |

## Codes

A code is a short, stable `snake_case` word (`invalid_airport_code`, `read_write_permission_required`, `hotel_not_bookable_in_sandbox`). Three kinds exist:

- **Specific codes** name the problem. They are listed in the [catalogue](#every-error-code) below and on the page of each endpoint. New ones can be added; the ones that exist are never renamed.
- **Generic codes** (`bad_request`, `not_found`, `conflict`, `internal_error` ...) stand for the HTTP status when no more specific code applies. Read the `message`.
- **Supplier codes** pass through what the airline, hotel or eSIM supplier answered, with `type: "supplier_error"`: sometimes a number (`"4926"`), sometimes a word (`validation_type`). They are not in the catalogue because the suppliers own them. Handle them by `type`, and show the `message` when you need to tell a human.

Write your handler with a `switch` on the codes you care about and a default that falls back on `type`.

## HTTP statuses

| Status | Meaning |
|---|---|
| `200` | Success. Creations answer 200 too, not 201. A few post-booking endpoints also use it for supplier refusals: check `success`. |
| `202` | The eSIM catalogue or device list is still being prepared (`success: false`, empty `data`). Retry in a few seconds. |
| `207` | Partial success: an ACD document upload stored some files and not others. `success` is `true`; read `results`. |
| `400` | The request is not valid (invalid JSON, a missing or wrong field, a business rule). The bad value is rejected before any supplier is called. |
| `401` | No key, a malformed key or an unknown key. |
| `402` | Insufficient balance (live only). |
| `403` | The key may not do this (permission, IP address, agency not approved). |
| `404` | No such route or object for your agency. |
| `405` | The route exists but not for this HTTP method. |
| `409` | The request conflicts with the state of the object (duplicate, already done). |
| `410` | An offer has expired: search and price again. |
| `413` | The body is larger than the endpoint accepts. |
| `415` | The body is not in a format the endpoint accepts (for example an ACD upload that is not multipart). |
| `422` | A query or path parameter is missing or has the wrong type or range. One entry per problem in `errors`. |
| `429` | Too many requests. Wait for `Retry-After`. |
| `500` | An unexpected error on our side. |
| `502`, `503`, `504` | A supplier or the service is temporarily unavailable. Retry with backoff. |

## Errors that do not come from the API

The API is served through Cloudflare. A few answers are generated before your request reaches FX-Port, and they are not JSON:

- **`403` with an HTML page and "error code: 1010"**: Cloudflare blocked the default `User-Agent` of your HTTP library. The default agents of Python's `urllib` and of Perl's `libwww-perl` are blocked. Send a `User-Agent` of your own (for example `my-company-booking/1.0`). The other clients we tested work with their default agent: curl, Wget, Python `requests`, `httpx` and `aiohttp`, Node `fetch` and axios, Go, Java, OkHttp, Apache HttpClient, PHP Guzzle, Ruby, Dart, Postman, Insomnia and HTTPie.
- **`301` from `http://`**: use `https://` only.
- **`307` without a body** on a URL that ends with a slash: remove the trailing slash.
- **`502`, `504` or `524` with an HTML page**: the edge could not reach the API in time. Treat them like any other temporary failure and retry with backoff.

Parse the body only when the `Content-Type` is `application/json`.

## Retrying safely

| Call | Safe to repeat? |
|---|---|
| Searches, prices, rates, lookups, listings (`GET` and read `POST`) | Yes. |
| `book_flight`, `pay_order`, `cancel_order`, `confirm_cancel`, `book_hotel`, `cancel_hotel`, eSIM `orders` and `topups` | **No, not blindly.** The first call may have succeeded although the answer never reached you. Read the booking back first (`get_booking`, `GET /bookings/{id}`) and repeat only if it does not exist. FX-Port rejects an obvious duplicate with `409`, but that is a safety net, not an idempotency key: there is no `Idempotency-Key` header. |

For everything you may repeat, use exponential backoff with jitter and honour `Retry-After`:


**Python**

~~~python
import random
import time

import requests

RETRYABLE = {429, 500, 502, 503, 504}


def call_with_retry(method, url, attempts=5, **kwargs):
    for attempt in range(attempts):
        response = requests.request(method, url, timeout=90, **kwargs)
        if response.status_code not in RETRYABLE or attempt == attempts - 1:
            return response
        wait = float(response.headers.get("Retry-After", 0)) or min(30, 2 ** attempt)
        time.sleep(wait + random.uniform(0, 1))
~~~

**JavaScript**

~~~js
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function callWithRetry(url, options, attempts = 5) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    const response = await fetch(url, options);
    if (!RETRYABLE.has(response.status) || attempt === attempts - 1) return response;
    const wait = Number(response.headers.get("retry-after")) || Math.min(30, 2 ** attempt);
    await new Promise((resolve) => setTimeout(resolve, (wait + Math.random()) * 1000));
  }
}
~~~


## Older shapes

Before `errors` existed, each part of the API had its own error body. They are all still returned, next to `errors`:

| Shape | Seen on |
|---|---|
| `{"detail": "text"}` | Many flight and hotel errors, `404`, `405` |
| `{"detail": {"error": "code_or_text", "message": "..."}}` | Authentication, permissions, account, flights, hotels |
| `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}` | Validation of query and path parameters (`422`) |
| `{"success": false, "error": "text"}` | eSIM endpoints |
| `{"success": false, "error": "code", "message": "...", "details": {...}}` | eSIM errors that have a code (for example `insufficient_balance`) |
| `{"success": false, "status": "failed", "requestId": "...", "error": "code", "code": "CODE", "message": "..."}` | ACD document endpoints |
| `{"success": false, "error": {"message": "...", "code": "...", "type": "...", "details": [...]}}` | A supplier's own refusal passed through (flights) |
| `{"detail": {"errors": [...], "meta": {...}}}` | `429`, nested under `detail` |

## Report a problem

Write to [dev@fx-port.com](mailto:dev@fx-port.com) and include the `X-Request-Id` header (or the `request_id` of the body), the environment, the endpoint and the time. With the request id we can find your request immediately.

## Every error code

The catalogue below lists every code the API can answer, its HTTP status, its type and what it means.


| Code | HTTP status | Type | Meaning |
| --- | --- | --- | --- |
| `already_cancelled` | 200 | `supplier_error` | The order was cancelled before (a `200` with `success: false`). |
| `order_not_cancellable` | 200 | `supplier_error` | The airline or the fare does not allow cancelling the order through the API (a `200` with `success: false`). |
| `order_type_not_eligible_for_payment` | 200 | `supplier_error` | A Duffel order that was bought instantly cannot be paid later (a `200` with `success: false`). |
| `request_failed` | 200 | `supplier_error` | The call answered HTTP 200 with success false and no more specific code: read the message. |
| `agency_id_not_allowed` | 400 | `validation_error` | An API key sent `agency_id`. The agency is taken from the key: remove the parameter. |
| `bad_request` | 400 | `validation_error` | The request is not valid and no more specific code is available; read the message. |
| `booking_mode_not_available` | 400 | `validation_error` | The body of `book_hotel` has a `booking_mode` other than `standard`. A booking made with an API key always books and confirms the rate: do not send `booking_mode`. |
| `checkout_before_checkin` | 400 | `validation_error` | `checkout` is not after `checkin`. |
| `date_in_past` | 400 | `validation_error` | `checkin` is before yesterday (yesterday is accepted for time zones). |
| `date_too_far` | 400 | `validation_error` | `checkin` is more than 400 days ahead. `errors[0].details.max_days_ahead` is 400. |
| `document_count_mismatch` | 400 | `validation_error` | `traveler_ids` and `documents` have different lengths: they are matched by order. The body has `received_traveler_ids` and `received_documents`. |
| `duplicate_traveler_id` | 400 | `validation_error` | A traveler appears twice in one upload. The body has `duplicate_traveler_ids`. |
| `empty_file` | 400 | `validation_error` | A file has no content. |
| `hold_not_supported` | 400 | `validation_error` | `booking_type: hold` on an offer that cannot be held: read `supportHold` on the priced offer. |
| `incorrect_guests_number` | 400 | `supplier_error` | The guests of `rooms` do not match the searched occupancy, or a child lacks `is_child` and `age`. |
| `invalid_adult_count` | 400 | `validation_error` | A room has fewer than 1 or more than 6 adults, or `adults` is not a whole number (a whole number written as text is read). |
| `invalid_agent_id` | 400 | `validation_error` | `agent_id` is not the UUID of a user of your agency. |
| `invalid_airline_filter` | 400 | `validation_error` | An airline list has more than 20 entries or an entry that is not an IATA airline code. |
| `invalid_airport_code` | 400 | `validation_error` | An airport or city code is not three ASCII letters (`origin` and `destination` take up to five codes, as `"ALG,ORN"` or a list; a segment takes one), or the destination equals the origin. |
| `invalid_body` | 400 | `validation_error` | The body is valid JSON but not the kind of value the endpoint expects (for example an array instead of an object). |
| `invalid_booking_id` | 400 | `validation_error` | The booking reference is not a valid booking id: it is empty, too long (100 characters on the eSIM and account endpoints, 256 on the hotel ones) or has characters a booking id never has (letters, digits and `.`, `_`, `:`, `-` are allowed, `=`, `+` and `~` too on the eSIM and account endpoints). |
| `invalid_booking_type` | 400 | `validation_error` | `booking_type` is not `issue` or `hold` (any letter case). An unknown value never becomes an order. |
| `invalid_cabin_class` | 400 | `validation_error` | `cabin_class` is not one of `economy`, `premium_economy`, `business`, `first`, `any`, once case, spaces, dashes and underscores are ignored (`Premium Economy`, `premium-economy` and `premium` are read as `premium_economy`). `errors[0].details.allowed` lists the values. |
| `invalid_cancellation_id` | 400 | `validation_error` | `cancellation_id` is not a valid token. |
| `invalid_child_age` | 400 | `validation_error` | A child age is not a whole number from 0 to 17 (a whole number written as text is read). |
| `invalid_commission_override` | 400 | `validation_error` | `commission_override` is not `{ type: percentage \| fixed, value >= 0 }`. |
| `invalid_country` | 400 | `validation_error` | `country` is neither a country slug such as `france` nor a country name such as `United States`: it has punctuation, control characters or non-Latin letters. |
| `invalid_country_code` | 400 | `validation_error` | `from_cc`, `to_cc` or an entry of `countries` is not exactly two ASCII letters; or the `issuing_country` of an identity document is no country value (2 to 4 letters, optionally `-` and 1 to 3 letters or digits: `DZ`, `GB-SCT`, `USPR`). |
| `invalid_currency` | 400 | `validation_error` | A currency override is not a three-letter ISO 4217 code. |
| `invalid_date` | 400 | `validation_error` | A date is not a real calendar date written `YYYY-MM-DD`. A date written with a midnight time (`2026-12-01T00:00:00Z`) is read as that day, not refused; any other time of day is. |
| `invalid_date_range` | 400 | `validation_error` | `date_from` is after `date_to`. |
| `invalid_description` | 400 | `validation_error` | `description` is not a string of at most 500 characters. |
| `invalid_detail_level` | 400 | `validation_error` | `detail_level` is not `minimal` (alias `mini`) or `full`. |
| `invalid_email` | 400 | `validation_error` | `email` is not an email address (at most 254 characters). |
| `invalid_field_type` | 400 | `validation_error` | A field has the wrong JSON type. `errors[0].details` names the expected and the received type. |
| `invalid_fields` | 400 | `validation_error` | `fields` is not a list of at most 40 top-level field names separated by commas. |
| `invalid_gender` | 400 | `validation_error` | A passenger `gender` is none of `M`, `F`, `MALE`, `FEMALE` (any letter case), `X`, `U` and `UNSPECIFIED` (a passport marked X). |
| `invalid_guests` | 400 | `validation_error` | `guests` is not a non-empty list with one `{adults, children}` object per room. |
| `invalid_hotel_id` | 400 | `validation_error` | A hotel id is not 1 to 128 characters: letters, digits and `_ . : -` (spaces around it are trimmed; a positive JSON integer, the numeric supplier id, is read as its text). |
| `invalid_hotel_ids` | 400 | `validation_error` | `hotel_ids` is not a list of hotel ids. |
| `invalid_iccid` | 400 | `validation_error` | The ICCID is not 18 to 22 digits. Groups of spaces or hyphens are dropped first, but any other character makes it invalid. |
| `invalid_identifier` | 400 | `validation_error` | An id is not a valid token: letters, digits and `- _ . : = ~`, 1 to 128 characters. |
| `invalid_images_limit` | 400 | `validation_error` | `images_limit` is not an integer from 0 to 30 (`get_hotel_info`, `POST /hotels/search` and `POST /hotels/details`). `errors[0].details` carries `min` and `max`. |
| `invalid_is_child` | 400 | `validation_error` | `is_child` of a guest of `book_hotel` is not `true` or `false` (the text `true` or `false`, `1` and `0` are read as the boolean). `errors[0].field` names the guest, for example `rooms[0].guests[1].is_child`. |
| `invalid_json` | 400 | `validation_error` | The body is not valid JSON. |
| `invalid_language` | 400 | `validation_error` | `language` is not a language code such as `en`, `fr` or `zh_CN`. A longer tag such as `zh-Hans-CN` or `es-419` is read; `en-USA`, a 3-letter code or a text with spaces is refused. |
| `invalid_latitude` | 400 | `validation_error` | `latitude` is not a number from -90 to 90 (a number written as text is read). |
| `invalid_limit` | 400 | `validation_error` | `limit` of the name search is not a whole number from 1 to 100 (a whole number written as text is read). `errors[0].details` carries `min` and `max` when the number is too large. `POST /hotels/search-by-name` caps a number above 100 (with a `coerced_value` warning); `GET /hotels/name-search` refuses it. |
| `invalid_locale` | 400 | `validation_error` | `locale` is not a language code or locale such as `en`, `zh-Hans`, `pt_BR` or `fr_FR.UTF-8` (or an `Accept-Language` value). |
| `invalid_longitude` | 400 | `validation_error` | `longitude` is not a number from -180 to 180 (a number written as text is read). |
| `invalid_min_score` | 400 | `validation_error` | `min_score` of the name search is not a number from 0 to 100 (a number written as text is read). `errors[0].details` carries `min` and `max`. |
| `invalid_multipart` | 400 | `validation_error` | The multipart body cannot be read, or has more than 40 files, 64 fields or a text field over 64 KiB. |
| `invalid_name` | 400 | `validation_error` | `first_name` or `last_name` is not a string of at most 100 characters. |
| `invalid_package_id` | 400 | `validation_error` | `package_id` is not a package id: 1 to 128 letters, digits, `-`, `_` or `.`. |
| `invalid_passenger_age` | 400 | `validation_error` | A passenger age is not a whole number from 0 to 17. Numeric text, integral floats and a fraction (`0.5`, read as the years completed) are read with a `coerced_value` warning; words, booleans, negatives and ages above 17 are refused. |
| `invalid_passenger_count` | 400 | `validation_error` | A passenger count is not a whole number from 0 to 99. Numeric text and integral floats (`"2"`, `2.0`) are read with a `coerced_value` warning; words, fractions, booleans, negatives and bigger numbers are refused. |
| `invalid_passenger_type` | 400 | `validation_error` | A passenger `type` is not one of `adult`, `senior`, `young_adult`, `child`, `seated_infant`, `infant` (or an accepted synonym). |
| `invalid_payment_type` | 400 | `validation_error` | `payment_type` is not `balance`, `card` or `arc_bsp_cash`. |
| `invalid_phone` | 400 | `validation_error` | `phone` has fewer than 3 digits or more than 32 characters. |
| `invalid_phone_number` | 400 | `validation_error` | `contact_details.phone_number` is not a phone number: digits with spaces, dots, dashes, slashes and brackets, one leading `+`, 5 to 24 digits and 32 characters at most. A dial code alone (`+213`) is refused. |
| `invalid_price_increase` | 400 | `validation_error` | `price_increase_percent` is not a number from 0 to 100 (a number written as text is read; `null` means not set). |
| `invalid_product` | 400 | `validation_error` | `product` is not one of `all`, `esims`, `flights`, `hotels`, `transfers` (any case; the singular names are read as the plural). The error lists them in `allowed`. |
| `invalid_quantity` | 400 | `validation_error` | `quantity` is not an integer between 1 and 50. The value is otherwise ignored: every order creates one eSIM. |
| `invalid_query` | 400 | `validation_error` | `query` is not a text of the right length: 2 to 100 characters for the name search, 1 to 64 for the location search (which answers this code with HTTP 422). |
| `invalid_radius` | 400 | `validation_error` | `radius` is not a whole number of meters (a boolean, a fraction, `1e3`, text that is not plain digits). A whole number written as text is read; a number outside 100 to 70,000 is not an error: it is moved to the nearest bound. |
| `invalid_rate_hash` | 400 | `validation_error` | `book_hash` is not a rate token taken from `get_rates`. |
| `invalid_replace_flag` | 400 | `validation_error` | `replace` is not `true` or `false` (`1`, `yes`, `on`, `0`, `no`, `off` are read too). |
| `invalid_request` | 400 | `validation_error` | The body of `book_hotel` names two booking ids: `partner.partner_order_id` differs from `booking_id`. Send the booking id once, as `booking_id`. (A `book_hash` or `hotel_id` that differs from the pre-booked offer is not an error: it is ignored, with an `ignored_field` warning.) |
| `invalid_residency` | 400 | `validation_error` | `residency` is not a two-letter ISO 3166-1 country code the hotel supplier knows (`zz`, `usa`, `gbr`, an empty text). A sub-territory code such as `GB-SCT`, `USPR` or `PT-MA` is read as its country, not refused. |
| `invalid_rooms` | 400 | `validation_error` | `rooms` of `book_hotel` is not a non-empty list of `{guests: [...]}` objects with 1 to 10 guests each. |
| `invalid_search` | 400 | `validation_error` | `search` is longer than 100 characters or contains control characters. |
| `invalid_segments` | 400 | `validation_error` | `segments` is not a list of 1 to 6 flights. |
| `invalid_send_email` | 400 | `validation_error` | `send_email` of `book_hotel` is not `true` or `false`. The text `true` or `false` (also `1`, `0`, `yes`, `no`, `on`, `off`) is read as the boolean, `null` means the default; any other value is refused before anything is booked. |
| `invalid_status` | 400 | `validation_error` | The booking is not `confirmed`: only a confirmed booking can be cancelled. `pending`, `failed` and already cancelled bookings cannot. |
| `invalid_supplier_selection` | 400 | `validation_error` | A `supplier_id` is unknown, disabled, not a valid token or cannot do what the call needs (flexible search). `availableSuppliers` lists the usable ids. |
| `invalid_type` | 400 | `validation_error` | `type` is not a short name such as `local` or `global`. |
| `invalid_url` | 400 | `validation_error` | The `url` of a search is longer than 512 characters or is not a path-style search. |
| `missing_booking_id` | 400 | `validation_error` | No `booking_id` (a JSON field for `get_acd_documents`, a form field for `upload_acd_documents`). |
| `missing_documents` | 400 | `validation_error` | An upload without a file in `documents`. |
| `missing_field` | 400 | `validation_error` | A required field is missing. |
| `missing_search_params` | 400 | `validation_error` | A search names neither a `url`, nor `segments`, nor the three fields `origin`, `destination` and `departure_date`. |
| `missing_traveler_ids` | 400 | `validation_error` | An upload without `traveler_ids`. |
| `order_not_found` | 400 | `supplier_error` | The supplier has no order for this booking id: `pre_book` did not open one (the hotel is not bookable) or the order expired. |
| `rate_not_found` | 400 | `supplier_error` | The supplier does not know the rate token any more: the rate expired, was never valid, or its price rose by more than `price_increase_percent`. Call `get_rates` again. |
| `return_before_departure` | 400 | `validation_error` | The return date, or the date of a later segment, is before the previous flight. |
| `stay_too_long` | 400 | `validation_error` | The stay is longer than 30 nights, the longest the supplier prices. |
| `supplier_passenger_combination_unsupported` | 400 | `supplier_error` | No selected supplier can serve the passenger mix (some suppliers need one adult or senior for every passenger under two). |
| `too_many_children` | 400 | `validation_error` | A room has more than 4 children. |
| `too_many_documents` | 400 | `validation_error` | More files than travelers on the booking. The body has `expected_travelers` and `received_documents`. |
| `too_many_guests` | 400 | `validation_error` | A room has more than 10 guests. |
| `too_many_iccids` | 400 | `validation_error` | `iccids` lists more ICCIDs than an API key may ask for at once (50). |
| `too_many_ids` | 400 | `validation_error` | More than 200 hotel ids in `hotel_ids` of `get_hotel_info`. |
| `too_many_passengers` | 400 | `validation_error` | A booking has more than 18 passengers. |
| `too_many_rooms` | 400 | `validation_error` | More than 9 rooms in one request. |
| `traveler_rule_violation` | 400 | `validation_error` | The passenger mix breaks a rule: more than 9 seated passengers, more lap infants than adults and seniors, or more seated infants than adults and seniors. The body keeps its older flat shape (`error: TRAVELER_RULE_VIOLATION`) with `violations[]`. |
| `unknown_traveler_id` | 400 | `validation_error` | A traveler id is not on the booking. `results[].error.expected_traveler_ids` lists the valid ones. |
| `unsupported_supplier` | 400 | `validation_error` | `get_ticket_number`: the booking is not an Amadeus, Travelport or Duffel booking, so its e-ticket number cannot be asked. Read the booking with `get_booking`. |
| `validation_failed` | 400 | `validation_error` | Several files of one upload have different problems; nothing was stored. Read `results[].error`. |
| `authentication_required` | 401 | `authentication_error` | No API key was sent. |
| `invalid_api_key` | 401 | `authentication_error` | The key is unknown, revoked or expired. |
| `invalid_api_key_format` | 401 | `authentication_error` | The key does not have the shape of an FX-Port key: fxp_live_ or fxp_test_ followed by 16 to 64 letters and digits. |
| `invalid_token` | 401 | `authentication_error` | The bearer token is neither an API key nor a valid dashboard session (eSIM endpoints accept both). |
| `unauthorized` | 401 | `authentication_error` | The request is not authenticated and no more specific reason is available. |
| `insufficient_balance` | 402 | `payment_error` | The balance of your agency is lower than the amount to debit. Live only: the sandbox debits nothing. Top up the balance, then repeat the request. |
| `acd_exemption_currency` | 403 | `permission_error` | ACD exemptions exist only for agencies with a DZD account. |
| `acd_exemption_invalid` | 403 | `permission_error` | `acd_exempt_company_id` is not linked to your agency, or is no longer approved. A search with it answers `403`; `book_flight` answers `409` with the same code when the exemption of the search is not valid any more (search again without it). |
| `agency_not_active` | 403 | `permission_error` | The agency of the key is not approved for this environment yet. |
| `endpoint_not_available` | 403 | `permission_error` | The endpoint is for FX-Port's own applications and is not available to API keys. |
| `forbidden` | 403 | `permission_error` | The key may not do this and no more specific reason is available. |
| `ip_not_allowed` | 403 | `permission_error` | The key has an IP allow-list and the request came from another address. |
| `read_write_permission_required` | 403 | `permission_error` | The endpoint changes data and the key has read permission only. |
| `balance_not_found` | 404 | `not_found` | No balance of your agency has this id (live). The sandbox has no balances and answers any id with an empty list. |
| `booking_not_found` | 404 | `not_found` | No booking of your agency in this environment has this reference (for the product asked, when `product` is set). |
| `hotel_not_found` | 404 | `not_found` | The supplier has no hotel with this id. |
| `iccid_not_found` | 404 | `not_found` | The ICCID is not an eSIM that your agency ordered in this environment. |
| `not_found` | 404 | `not_found` | No such route or object. |
| `offer_not_found` | 404 | `not_found` | The offer is not in that search: it comes from another supplier entry or another search. |
| `offer_not_priced` | 404 | `not_found` | The offer was never priced, or its priced version is older than 12 hours: call `price_flight` first. |
| `pdf_not_found` | 404 | `not_found` | The booking exists but its voucher is not made yet. It is made right after the booking is `confirmed`, sometimes up to 10 seconds later: call `get_voucher` again in 2 seconds. |
| `search_not_found` | 404 | `not_found` | The `search_id` is unknown, older than 24 hours, a `requestId`, or belongs to another agency (the answer is the same). |
| `method_not_allowed` | 405 | `validation_error` | The route exists but not for this HTTP method. |
| `acd_not_required` | 409 | `conflict` | The booking does not need ACD documents (`get_acd_documents` answers `200` for it; only the upload is refused). |
| `acd_waived` | 409 | `conflict` | FX-Port marked ACD as not required for this booking. |
| `booking_in_progress` | 409 | `conflict` | Another request is booking this offer right now. Read the booking before you retry. |
| `booking_lock_failed` | 409 | `conflict` | The offer could not be locked for booking. Read the booking before you retry. |
| `booking_not_eligible` | 409 | `conflict` | The booking is cancelled, failed, voided, refunded or expired. The body has `booking_status`. |
| `cached_offer_required` | 409 | `conflict` | The offer locked by `pre_book` is not found: it is older than one hour (live), or another agency locked it. Lock the rate again with `pre_book` and your own key, then book. `errors[0].details.partner_order_id` is the id you sent. |
| `concurrent_update` | 409 | `conflict` | Another request changed the documents of this booking at the same moment. Read the state and send what is still missing. |
| `conflict` | 409 | `conflict` | The request conflicts with the state of the object and no more specific code is available; read the message. |
| `document_already_uploaded` | 409 | `conflict` | The traveler already has a passport. Nothing was changed: send `replace=true` to replace it. |
| `duplicate_booking` | 409 | `conflict` | A booking already exists for this id. Do not call `book_hotel` again: read its status with `GET /bookings/{booking_id}?product=hotels`. `errors[0].details` carries the `booking_id` and, for the agency that owns the booking, its `status`. |
| `duplicate_request` | 409 | `conflict` | An order for the same package, or a topup of the same eSIM and package, is already being processed; or, on live, the latest confirmed order of your agency is for the same package (topup: the latest confirmed topup of the same eSIM) and is less than 60 seconds old. |
| `hotel_not_bookable_in_sandbox` | 409 | `conflict` | A sandbox key can book the supplier test hotels only: `test_hotel_do_not_book` (any residency) and `test_hotel` (residency `dz`, `ru` or `us`). |
| `invalid_booking_status` | 409 | `conflict` | The booking is not in the status the call needs (`cancel_order` needs `awaiting_payment`). |
| `offer_already_booked` | 409 | `supplier_error` | The supplier says this offer was already booked. |
| `offer_request_already_booked` | 409 | `supplier_error` | The supplier says this search (Duffel: one booking per offer request) was already booked. Search again. |
| `order_already_exists` | 409 | `supplier_error` | The supplier says an order for this booking already exists. |
| `travelers_not_available` | 409 | `conflict` | The booking has no traveler data, so files cannot be matched. Contact support. |
| `trusted_offer_pricing_missing` | 409 | `conflict` | The locked offer has no usable price: lock the rate again with `pre_book`. |
| `gone` | 410 | `not_found` | The object has expired (for example an offer): search and price again. |
| `offer_expired` | 410 | `not_found` | The supplier says the offer is gone. Search and price again. |
| `offer_incomplete` | 410 | `not_found` | The saved offer cannot be used any more. Search again. |
| `supplier_not_found` | 410 | `supplier_error` | The supplier of the saved offer is no longer available. Search again. |
| `technology_type_unknown` | 410 | `not_found` | The saved offer has a supplier technology FX-Port does not know any more. Search again. |
| `file_too_large` | 413 | `validation_error` | A file is over 10 MiB (10,485,760 bytes). The error has `max_file_size_bytes`. |
| `request_too_large` | 413 | `validation_error` | The body is larger than the endpoint accepts. |
| `unsupported_file_type` | 415 | `validation_error` | The content of a file is not a PDF or a supported image (JPEG, PNG, WebP, HEIC/HEIF, AVIF, TIFF, BMP, GIF, JPEG 2000, JPEG XL). The name and the `Content-Type` are not looked at. |
| `unsupported_media_type` | 415 | `validation_error` | The body is not in a format the endpoint accepts (for example an upload that is not multipart/form-data). |
| `born_on_does_not_match` | 422 | `supplier_error` | The date of birth does not match the passenger type of the offer (a child who is not 2 to 11 at departure, for example). |
| `no_supplier_supports_flexible_search` | 422 | `validation_error` | A flexible-only search on a route that no flexible-capable supplier serves (flexible dates exist on routes that touch Algeria). |
| `supplier_rejected_request` | 422 | `supplier_error` | The hotel supplier refused the request of `search_advanced` although it is well formed. Check dates, guests, residency and radius. `errors[0].details.supplier_code` carries the supplier's code when it has a plain one. |
| `validation_error` | 422 | `validation_error` | A query or path parameter is missing or has the wrong type or range; one item per problem in `errors`. |
| `validation_type` | 422 | `supplier_error` | The supplier refused a detail of the booking (a field has the wrong type or format). `errors[0].field` names it. |
| `auth_rate_limited` | 429 | `rate_limit_error` | Too many requests with a missing or invalid key came from your IP address. Requests with a valid key are never affected. |
| `concurrency_limit_exceeded` | 429 | `rate_limit_error` | Too many requests of this kind in flight at the same time for your key. Wait for Retry-After (a few seconds) or send fewer in parallel. |
| `rate_limit_exceeded` | 429 | `rate_limit_error` | More requests than the limit of this endpoint group per minute. See the Retry-After header. |
| `supplier_rate_limited` | 429 | `supplier_error` | The hotel supplier receives too many requests (`get_hotel_info`). Retry in a minute with backoff. |
| `balance_debit_failed` | 500 | `internal_error` | The supplier created the eSIM but the balance debit failed. Do not retry: contact support with the order code. |
| `booking_record_failed` | 500 | `internal_error` | The supplier created the eSIM but the booking could not be saved. Do not retry: contact support with the order code. |
| `booking_update_failed` | 500 | `internal_error` | The reservation was deleted but the FX-Port booking could not be updated. Read the booking and contact support with the request id. |
| `internal_error` | 500 | `internal_error` | An unexpected error on our side; quote the request_id. |
| `post_booking_failed` | 500 | `internal_error` | The booking was created at the supplier but FX-Port could not finish storing it. Do not book again: look for the booking in your history and quote the request id. |
| `supplier_response_invalid` | 500 | `supplier_error` | The supplier answered in a way that could not be processed. Do not retry: support has been alerted with the booking reference. |
| `bad_gateway` | 502 | `internal_error` | A supplier or an upstream service answered with an error. |
| `storage_upload_failed` | 502 | `internal_error` | No file could be stored. Nothing changed: retry the request. Each `results[].error` has the fixed `reason: storage_error`. |
| `supplier_error` | 502 | `supplier_error` | The hotel supplier could not process the request right now. Retry in a few minutes with backoff. |
| `supplier_invalid_response` | 502 | `supplier_error` | The hotel supplier answered in a form that could not be used. After `book_hotel` or `cancel_hotel` the call may have been processed: read the booking before you repeat it. On `pre_book` the same code with HTTP 400 means that the supplier priced the rate but returned no usable offer: call `get_rates` again. |
| `all_suppliers_failed` | 503 | `supplier_error` | No supplier returned results. `supplierErrors[]` lists each supplier with a stable `cause`. |
| `balance_lock_timeout` | 503 | `internal_error` | Another balance operation of your agency in the same currency is still running. Retry in a few seconds. |
| `duplicate_check_unavailable` | 503 | `internal_error` | The duplicate check could not run, so nothing was ordered (live). Retry in a few seconds. |
| `service_busy` | 503 | `internal_error` | The hotel name search is busy. Retry after the seconds in `Retry-After`. |
| `service_overloaded` | 503 | `internal_error` | The service is shedding load for a moment. Retry after the number of seconds in Retry-After. |
| `service_unavailable` | 503 | `internal_error` | The service or a supplier is temporarily unavailable. Retry with backoff. |
| `storage_unavailable` | 503 | `internal_error` | Document storage is not available. Retry later. |
| `supplier_api_error` | 503 | `supplier_error` | The supplier refused or failed on a price call. Retry once, otherwise search again. |
| `supplier_busy` | 503 | `supplier_error` | The supplier is saturated: its queue is full. Retry after the number of seconds in Retry-After. |
| `supplier_temporarily_unavailable` | 503 | `supplier_error` | The supplier is skipped for a short while after repeated failures. Wait, then retry. |
| `supplier_timeout` | 503 | `supplier_error` | The hotel supplier did not answer in time. A cancellation may still have happened: read the booking before you repeat `cancel_hotel`. |
| `supplier_unavailable` | 503 | `supplier_error` | The hotel supplier cannot be reached. Nothing was processed: retry in a few minutes with backoff. |
| `gateway_timeout` | 504 | `internal_error` | A supplier or an upstream service did not answer in time. |
