Errors
The error shapes of the API, every error code you can meet, request ids and what to do about each kind of error.
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
{
"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. |
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 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). |
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.
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. |
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 [email protected] 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 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 themessage. - 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 bytype, and show themessagewhen 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:
403with an HTML page and "error code: 1010": Cloudflare blocked the defaultUser-Agentof your HTTP library. The default agents of Python'surlliband of Perl'slibwww-perlare blocked. Send aUser-Agentof your own (for examplemy-company-booking/). The other clients we tested work with their default agent: curl, Wget, Python1.0 requests,httpxandaiohttp, Nodefetchand axios, Go, Java, OkHttp, Apache HttpClient, PHP Guzzle, Ruby, Dart, Postman, Insomnia and HTTPie.301fromhttp:/: use/ https:/only./ 307without a body on a URL that ends with a slash: remove the trailing slash.502,504or524with 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/.
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 /) 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:
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))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 [email protected] 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 | 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. |