Skip to content
FX-Port/Docs
GuidesAPI referenceChangelog
Get API keys
  • Introduction
  • Quickstart
  • Security
  • Authentication
  • Environments and Postman
  • Sandbox
  • Conventions and naming
  • Legacy names and twins
  • Errors
  • Rate limits and fair use
  • Streaming search results
  • Use these docs with AI

API fundamentals

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"
}
KeyMeaning
environmentsandbox or live: the environment of your key.
successAlways false for an error.
errorsThe errors, most important first. This is what you parse.
errors[].codeA stable, machine-readable code in snake_case. Branch on this, never on the message.
errors[].typeThe family of the error: it tells you how to react.
errors[].titleA short label.
errors[].messageA sentence for a human: what is wrong and how to fix it. The wording can change, the code does not.
errors[].fieldThe request field the error is about, or null.
errors[].detailsStructured extras, or null: for example available, required and currency on an insufficient balance.
errors[].documentation_urlThe entry of this code in the catalogue below.
meta, request_idThe id of the request: meta.request_id is the X-Request-Id header. Quote it when you write to support (see Request ids).

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 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.

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

typeWhat it meansWhat to do
authentication_errorThe key is missing, malformed, unknown or revoked.Fix the key or the way it is sent. Retrying the same request fails again.
permission_errorThe 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_errorThe request is wrong.Read errors[].field and message, fix the request, send it again. Do not retry unchanged.
not_foundThe object does not exist for your agency (or the id is wrong).Do not retry. Check the id and the environment of your key.
conflictThe 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_errorYour 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_errorToo many requests.Wait for the seconds in Retry-After, then retry with exponential backoff and jitter. See Rate limits.
supplier_errorThe 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_errorSomething 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 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

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

CallSafe 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 topupsNo, 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:

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:

ShapeSeen 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.

CodeHTTPTypeMeaning
already_cancelled200supplier_errorThe order was cancelled before (a `200` with `success: false`).
order_not_cancellable200supplier_errorThe airline or the fare does not allow cancelling the order through the API (a `200` with `success: false`).
order_type_not_eligible_for_payment200supplier_errorA Duffel order that was bought instantly cannot be paid later (a `200` with `success: false`).
request_failed200supplier_errorThe call answered HTTP 200 with success false and no more specific code: read the message.
agency_id_not_allowed400validation_errorAn API key sent `agency_id`. The agency is taken from the key: remove the parameter.
bad_request400validation_errorThe request is not valid and no more specific code is available; read the message.
booking_mode_not_available400validation_errorThe 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_checkin400validation_error`checkout` is not after `checkin`.
date_in_past400validation_error`checkin` is before yesterday (yesterday is accepted for time zones).
date_too_far400validation_error`checkin` is more than 400 days ahead. `errors[0].details.max_days_ahead` is 400.
document_count_mismatch400validation_error`traveler_ids` and `documents` have different lengths: they are matched by order. The body has `received_traveler_ids` and `received_documents`.
duplicate_traveler_id400validation_errorA traveler appears twice in one upload. The body has `duplicate_traveler_ids`.
empty_file400validation_errorA file has no content.
hold_not_supported400validation_error`booking_type: hold` on an offer that cannot be held: read `supportHold` on the priced offer.
incorrect_guests_number400supplier_errorThe guests of `rooms` do not match the searched occupancy, or a child lacks `is_child` and `age`.
invalid_adult_count400validation_errorA 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_id400validation_error`agent_id` is not the UUID of a user of your agency.
invalid_airline_filter400validation_errorAn airline list has more than 20 entries or an entry that is not an IATA airline code.
invalid_airport_code400validation_errorAn 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_body400validation_errorThe body is valid JSON but not the kind of value the endpoint expects (for example an array instead of an object).
invalid_booking_id400validation_errorThe 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_type400validation_error`booking_type` is not `issue` or `hold` (any letter case). An unknown value never becomes an order.
invalid_cabin_class400validation_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_id400validation_error`cancellation_id` is not a valid token.
invalid_child_age400validation_errorA child age is not a whole number from 0 to 17 (a whole number written as text is read).
invalid_commission_override400validation_error`commission_override` is not `{ type: percentage | fixed, value >= 0 }`.
invalid_country400validation_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_code400validation_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_currency400validation_errorA currency override is not a three-letter ISO 4217 code.
invalid_date400validation_errorA 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_range400validation_error`date_from` is after `date_to`.
invalid_description400validation_error`description` is not a string of at most 500 characters.
invalid_detail_level400validation_error`detail_level` is not `minimal` (alias `mini`) or `full`.
invalid_email400validation_error`email` is not an email address (at most 254 characters).
invalid_field_type400validation_errorA field has the wrong JSON type. `errors[0].details` names the expected and the received type.
invalid_fields400validation_error`fields` is not a list of at most 40 top-level field names separated by commas.
invalid_gender400validation_errorA passenger `gender` is none of `M`, `F`, `MALE`, `FEMALE` (any letter case), `X`, `U` and `UNSPECIFIED` (a passport marked X).
invalid_guests400validation_error`guests` is not a non-empty list with one `{adults, children}` object per room.
invalid_hotel_id400validation_errorA 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_ids400validation_error`hotel_ids` is not a list of hotel ids.
invalid_iccid400validation_errorThe ICCID is not 18 to 22 digits. Groups of spaces or hyphens are dropped first, but any other character makes it invalid.
invalid_identifier400validation_errorAn id is not a valid token: letters, digits and `- _ . : = ~`, 1 to 128 characters.
invalid_images_limit400validation_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_child400validation_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_json400validation_errorThe body is not valid JSON.
invalid_language400validation_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_latitude400validation_error`latitude` is not a number from -90 to 90 (a number written as text is read).
invalid_limit400validation_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_locale400validation_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_longitude400validation_error`longitude` is not a number from -180 to 180 (a number written as text is read).
invalid_min_score400validation_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_multipart400validation_errorThe multipart body cannot be read, or has more than 40 files, 64 fields or a text field over 64 KiB.
invalid_name400validation_error`first_name` or `last_name` is not a string of at most 100 characters.
invalid_package_id400validation_error`package_id` is not a package id: 1 to 128 letters, digits, `-`, `_` or `.`.
invalid_passenger_age400validation_errorA 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_count400validation_errorA 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_type400validation_errorA passenger `type` is not one of `adult`, `senior`, `young_adult`, `child`, `seated_infant`, `infant` (or an accepted synonym).
invalid_payment_type400validation_error`payment_type` is not `balance`, `card` or `arc_bsp_cash`.
invalid_phone400validation_error`phone` has fewer than 3 digits or more than 32 characters.
invalid_phone_number400validation_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_increase400validation_error`price_increase_percent` is not a number from 0 to 100 (a number written as text is read; `null` means not set).
invalid_product400validation_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_quantity400validation_error`quantity` is not an integer between 1 and 50. The value is otherwise ignored: every order creates one eSIM.
invalid_query400validation_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_radius400validation_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_hash400validation_error`book_hash` is not a rate token taken from `get_rates`.
invalid_replace_flag400validation_error`replace` is not `true` or `false` (`1`, `yes`, `on`, `0`, `no`, `off` are read too).
invalid_request400validation_errorThe 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_residency400validation_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_rooms400validation_error`rooms` of `book_hotel` is not a non-empty list of `{guests: [...]}` objects with 1 to 10 guests each.
invalid_search400validation_error`search` is longer than 100 characters or contains control characters.
invalid_segments400validation_error`segments` is not a list of 1 to 6 flights.
invalid_send_email400validation_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_status400validation_errorThe booking is not `confirmed`: only a confirmed booking can be cancelled. `pending`, `failed` and already cancelled bookings cannot.
invalid_supplier_selection400validation_errorA `supplier_id` is unknown, disabled, not a valid token or cannot do what the call needs (flexible search). `availableSuppliers` lists the usable ids.
invalid_type400validation_error`type` is not a short name such as `local` or `global`.
invalid_url400validation_errorThe `url` of a search is longer than 512 characters or is not a path-style search.
missing_booking_id400validation_errorNo `booking_id` (a JSON field for `get_acd_documents`, a form field for `upload_acd_documents`).
missing_documents400validation_errorAn upload without a file in `documents`.
missing_field400validation_errorA required field is missing.
missing_search_params400validation_errorA search names neither a `url`, nor `segments`, nor the three fields `origin`, `destination` and `departure_date`.
missing_traveler_ids400validation_errorAn upload without `traveler_ids`.
order_not_found400supplier_errorThe 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_found400supplier_errorThe 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_departure400validation_errorThe return date, or the date of a later segment, is before the previous flight.
stay_too_long400validation_errorThe stay is longer than 30 nights, the longest the supplier prices.
supplier_passenger_combination_unsupported400supplier_errorNo selected supplier can serve the passenger mix (some suppliers need one adult or senior for every passenger under two).
too_many_children400validation_errorA room has more than 4 children.
too_many_documents400validation_errorMore files than travelers on the booking. The body has `expected_travelers` and `received_documents`.
too_many_guests400validation_errorA room has more than 10 guests.
too_many_iccids400validation_error`iccids` lists more ICCIDs than an API key may ask for at once (50).
too_many_ids400validation_errorMore than 200 hotel ids in `hotel_ids` of `get_hotel_info`.
too_many_passengers400validation_errorA booking has more than 18 passengers.
too_many_rooms400validation_errorMore than 9 rooms in one request.
traveler_rule_violation400validation_errorThe 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_id400validation_errorA traveler id is not on the booking. `results[].error.expected_traveler_ids` lists the valid ones.
unsupported_supplier400validation_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_failed400validation_errorSeveral files of one upload have different problems; nothing was stored. Read `results[].error`.
authentication_required401authentication_errorNo API key was sent.
invalid_api_key401authentication_errorThe key is unknown, revoked or expired.
invalid_api_key_format401authentication_errorThe 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_token401authentication_errorThe bearer token is neither an API key nor a valid dashboard session (eSIM endpoints accept both).
unauthorized401authentication_errorThe request is not authenticated and no more specific reason is available.
insufficient_balance402payment_errorThe 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_currency403permission_errorACD exemptions exist only for agencies with a DZD account.
acd_exemption_invalid403permission_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_active403permission_errorThe agency of the key is not approved for this environment yet.
endpoint_not_available403permission_errorThe endpoint is for FX-Port's own applications and is not available to API keys.
forbidden403permission_errorThe key may not do this and no more specific reason is available.
ip_not_allowed403permission_errorThe key has an IP allow-list and the request came from another address.
read_write_permission_required403permission_errorThe endpoint changes data and the key has read permission only.
balance_not_found404not_foundNo balance of your agency has this id (live). The sandbox has no balances and answers any id with an empty list.
booking_not_found404not_foundNo booking of your agency in this environment has this reference (for the product asked, when `product` is set).
hotel_not_found404not_foundThe supplier has no hotel with this id.
iccid_not_found404not_foundThe ICCID is not an eSIM that your agency ordered in this environment.
not_found404not_foundNo such route or object.
offer_not_found404not_foundThe offer is not in that search: it comes from another supplier entry or another search.
offer_not_priced404not_foundThe offer was never priced, or its priced version is older than 12 hours: call `price_flight` first.
pdf_not_found404not_foundThe 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_found404not_foundThe `search_id` is unknown, older than 24 hours, a `requestId`, or belongs to another agency (the answer is the same).
method_not_allowed405validation_errorThe route exists but not for this HTTP method.
acd_not_required409conflictThe booking does not need ACD documents (`get_acd_documents` answers `200` for it; only the upload is refused).
acd_waived409conflictFX-Port marked ACD as not required for this booking.
booking_in_progress409conflictAnother request is booking this offer right now. Read the booking before you retry.
booking_lock_failed409conflictThe offer could not be locked for booking. Read the booking before you retry.
booking_not_eligible409conflictThe booking is cancelled, failed, voided, refunded or expired. The body has `booking_status`.
cached_offer_required409conflictThe 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_update409conflictAnother request changed the documents of this booking at the same moment. Read the state and send what is still missing.
conflict409conflictThe request conflicts with the state of the object and no more specific code is available; read the message.
document_already_uploaded409conflictThe traveler already has a passport. Nothing was changed: send `replace=true` to replace it.
duplicate_booking409conflictA 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_request409conflictAn 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_sandbox409conflictA 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_status409conflictThe booking is not in the status the call needs (`cancel_order` needs `awaiting_payment`).
offer_already_booked409supplier_errorThe supplier says this offer was already booked.
offer_request_already_booked409supplier_errorThe supplier says this search (Duffel: one booking per offer request) was already booked. Search again.
order_already_exists409supplier_errorThe supplier says an order for this booking already exists.
travelers_not_available409conflictThe booking has no traveler data, so files cannot be matched. Contact support.
trusted_offer_pricing_missing409conflictThe locked offer has no usable price: lock the rate again with `pre_book`.
gone410not_foundThe object has expired (for example an offer): search and price again.
offer_expired410not_foundThe supplier says the offer is gone. Search and price again.
offer_incomplete410not_foundThe saved offer cannot be used any more. Search again.
supplier_not_found410supplier_errorThe supplier of the saved offer is no longer available. Search again.
technology_type_unknown410not_foundThe saved offer has a supplier technology FX-Port does not know any more. Search again.
file_too_large413validation_errorA file is over 10 MiB (10,485,760 bytes). The error has `max_file_size_bytes`.
request_too_large413validation_errorThe body is larger than the endpoint accepts.
unsupported_file_type415validation_errorThe 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_type415validation_errorThe body is not in a format the endpoint accepts (for example an upload that is not multipart/form-data).
born_on_does_not_match422supplier_errorThe 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_search422validation_errorA flexible-only search on a route that no flexible-capable supplier serves (flexible dates exist on routes that touch Algeria).
supplier_rejected_request422supplier_errorThe 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_error422validation_errorA query or path parameter is missing or has the wrong type or range; one item per problem in `errors`.
validation_type422supplier_errorThe supplier refused a detail of the booking (a field has the wrong type or format). `errors[0].field` names it.
auth_rate_limited429rate_limit_errorToo many requests with a missing or invalid key came from your IP address. Requests with a valid key are never affected.
concurrency_limit_exceeded429rate_limit_errorToo 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_exceeded429rate_limit_errorMore requests than the limit of this endpoint group per minute. See the Retry-After header.
supplier_rate_limited429supplier_errorThe hotel supplier receives too many requests (`get_hotel_info`). Retry in a minute with backoff.
balance_debit_failed500internal_errorThe supplier created the eSIM but the balance debit failed. Do not retry: contact support with the order code.
booking_record_failed500internal_errorThe supplier created the eSIM but the booking could not be saved. Do not retry: contact support with the order code.
booking_update_failed500internal_errorThe reservation was deleted but the FX-Port booking could not be updated. Read the booking and contact support with the request id.
internal_error500internal_errorAn unexpected error on our side; quote the request_id.
post_booking_failed500internal_errorThe 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_invalid500supplier_errorThe supplier answered in a way that could not be processed. Do not retry: support has been alerted with the booking reference.
bad_gateway502internal_errorA supplier or an upstream service answered with an error.
storage_upload_failed502internal_errorNo file could be stored. Nothing changed: retry the request. Each `results[].error` has the fixed `reason: storage_error`.
supplier_error502supplier_errorThe hotel supplier could not process the request right now. Retry in a few minutes with backoff.
supplier_invalid_response502supplier_errorThe 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_failed503supplier_errorNo supplier returned results. `supplierErrors[]` lists each supplier with a stable `cause`.
balance_lock_timeout503internal_errorAnother balance operation of your agency in the same currency is still running. Retry in a few seconds.
duplicate_check_unavailable503internal_errorThe duplicate check could not run, so nothing was ordered (live). Retry in a few seconds.
service_busy503internal_errorThe hotel name search is busy. Retry after the seconds in `Retry-After`.
service_overloaded503internal_errorThe service is shedding load for a moment. Retry after the number of seconds in Retry-After.
service_unavailable503internal_errorThe service or a supplier is temporarily unavailable. Retry with backoff.
storage_unavailable503internal_errorDocument storage is not available. Retry later.
supplier_api_error503supplier_errorThe supplier refused or failed on a price call. Retry once, otherwise search again.
supplier_busy503supplier_errorThe supplier is saturated: its queue is full. Retry after the number of seconds in Retry-After.
supplier_temporarily_unavailable503supplier_errorThe supplier is skipped for a short while after repeated failures. Wait, then retry.
supplier_timeout503supplier_errorThe hotel supplier did not answer in time. A cancellation may still have happened: read the booking before you repeat `cancel_hotel`.
supplier_unavailable503supplier_errorThe hotel supplier cannot be reached. Nothing was processed: retry in a few minutes with backoff.
gateway_timeout504internal_errorA supplier or an upstream service did not answer in time.
PreviousLegacy names and twinsNext Rate limits and fair use

On this page

  • The error body
  • Check success, not only the status
  • What to do about each type
  • Codes
  • HTTP statuses
  • Errors that do not come from the API
  • Retrying safely
  • Older shapes
  • Report a problem
  • Every error code

© 2026 FX-Port

TermsPrivacy[email protected]