Conventions and naming
Request and response conventions, the canonical snake_case names, and the legacy camelCase names that still work and how to move off them.
The rules below hold for every endpoint of flights, hotels, eSIMs and the account. Learn them once.
Transport and format
| Base URL | https:/. One host for sandbox and live: the key decides (Authentication). |
| Protocol | HTTPS only. http:/ is redirected with a 301. |
| Bodies | JSON in UTF-8 with Content-Type: application/. The one exception is the ACD document upload, which is multipart/. A JSON body sent without that content type is not read as JSON. |
| Methods | GET for lookups, catalogues and the account; POST for searches, prices, bookings and everything that takes a body. |
| Trailing slash | Do not add one. / answers 307 with an empty body. |
| Compression | Send Accept-Encoding: gzip. Flight searches are 0.7 to 14 MB of JSON, ten times smaller compressed. Streams are never compressed. |
| Caching | Responses are Cache-Control: private, no-store. Cache on your side what you can (Rate limits). The airport and destination typeaheads are the only cacheable answers. |
| Browsers | Do not call the API from a browser: CORS is enabled for FX-Port's own sites only, and a key in a browser is a leaked key. |
| User-Agent | Send your own. Cloudflare blocks the default agents of Python's urllib and Perl's libwww-perl (details). |
| Health | GET https:/ and / need no key and are outside /. |
| Headers you can read | X-Request-Id, X-Environment, ratelimit-limit, ratelimit-remaining, ratelimit-reset, ratelimit-policy, Retry-After, Deprecation, Link, X-FX-Naming, X-FX-Deprecated-Fields, X-FX-Ignored-Fields. |
| API host pages | The interactive documentation of the API host is off. This site is the documentation, and the OpenAPI file is at /openapi.json. |
Every JSON object answer starts with "environment": "sandbox" or "live", and every response carries X-Environment and X-Request-Id.
Naming
Names you send are snake_case. search_id, offer_id, booking_type, contact_details, phone_number, first_name, date_of_birth, cabin_class. The guides and the reference show these names and only these.
Over the years some endpoints took, or returned, other spellings: searchId, offerId, bookingType, contactDetails. Nothing was removed. Every previous spelling still works, and the API now tells you which one to use.
A previous spelling still works, and says so
When a request uses a previous spelling of a field, or a field the endpoint does not read, the answer carries a warnings array and headers that name what to use instead. The status code is not affected.
curl -s -X POST https://api.fx-port.com/api/v1/flights/price_flight \
-H "Authorization: Bearer fxp_test_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"searchId": "fs_dfx_441e54ae-d7fe-414f-9fba-5415ee5ffb76", "offerId": "fx_offer_f3d89262"}'HTTP/1.1 200 OK
Deprecation: true
Link: <https://fx-port.com/docs/conventions#naming>; rel="deprecation"
X-FX-Deprecated-Fields: searchId->search_id,offerId->offer_id{
"environment": "sandbox",
"success": true,
"warnings": [
{
"code": "deprecated_field",
"field": "searchId",
"replacement": "search_id",
"message": "`searchId` is a previous spelling; use `search_id`. The old name keeps working.",
"documentation_url": "https://fx-port.com/docs/conventions#naming"
},
{ "code": "deprecated_field", "field": "offerId", "replacement": "offer_id", "message": "..." }
],
"results": { "...": "..." }
}Warning code | Meaning | Header |
|---|---|---|
deprecated_field | The request used a previous spelling of a field. replacement is the name to use. | Deprecation, X-FX-Deprecated-Fields |
ignored_field | The request carried a field the endpoint does not read. It had no effect. | X-FX-Ignored-Fields |
coerced_value | A value was changed to a supported one; the message says how. | none |
If a field you meant to use shows up as ignored_field, that is usually a typo or a wrong spelling: the API never rejects unknown fields, so this is how you find out.
Responses keep their keys and add snake_case twins
Response keys that were camelCase stay exactly as they were. Next to the keys you feed back into later calls, or compute with, the response adds the snake_case twin, with the same value:
{
"requestId": "11fa601e",
"request_id": "11fa601e",
"suppliers": [{ "searchId": "fs_dfx_441e54ae-d7fe-414f-9fba-5415ee5ffb76", "search_id": "fs_dfx_441e54ae-d7fe-414f-9fba-5415ee5ffb76" }]
}Read the twin when it is there and fall back to the older key, so your code works with both:
search_id = supplier.get("search_id") or supplier["searchId"]The reference marks every previous spelling as deprecated and names its replacement. The complete lists, generated from the API itself, are in Legacy names and twins. Machine readers can fetch them from GET / (no key needed).
Get only snake_case
Add ?naming=snake to any request, or the header X-FX-Naming: snake, and every key of the answer is snake_case (requestId becomes request_id, b2bPrice becomes b2b_price). Values are never touched. The answer carries X-FX-Naming: snake. This is the simplest choice for a new integration written in a language that likes snake_case. Enumerated values keep the spelling documented for each endpoint.
One concept, one name
| Concept | Name | Notes |
|---|---|---|
| A flight search | search_id | Of the supplier entry the offer came from. Needed by price and book. |
| A flight offer | offer_id | The id of the offer object. |
| A supplier | supplier_id | For example duffel_1. Only searches take it as input; price and book derive it from the offer. |
| A booking | booking_id | Endpoints that look a booking up accept the PNR, the supplier's order id or the FX-Port id. |
| The airline reservation | pnr | The reference you give the traveler. |
| The supplier's order | order_id | Needed to pay or cancel a held order. |
| A hotel | hotel_id | The id of the hotel in the content, for example test_hotel_do_not_book. |
| A hotel rate | book_hash | The token of one rate, from get_rates to pre_book. |
| A hotel booking | booking_id | The same value as the partner_order_id of pre_book and book_hotel: it identifies the booking when you poll it, fetch its voucher and cancel it. |
| An eSIM | iccid | The id of the SIM card. |
| A request | request_id | Quote it to support. |
Identifiers
Ids are opaque strings. Do not parse them, do not assume a length, and do not treat one as a secret: a booking reference can be guessed or leaked, so check ownership in your own backend. The API only ever returns objects of your own agency: asking for the id of another agency is answered with 403 or 404, never with the data.
Money
- Amounts are JSON numbers in the currency of your agency, with two decimals of precision (
21679.66). Do not use floating point arithmetic for money: round to the currency's minor unit and compare in cents. - The supplier's own currency and the rate used are next to the prices (
supplierCurrency,agencyCurrency,exchangeRate), so you can show or audit both. - Prices come in the same names across products:
base_price(before taxes),taxes_and_fees,final_price(what the traveler pays),agency_commission(the commission your agency earns on it) andb2b_price(what your agency is debited:final_priceminusagency_commission). eSIM catalogues expose the same idea asbalance_debit_amount. - In the sandbox no balance is debited and
GET /is empty.balances
Dates and times
- Dates are
YYYY-MM-DDand are the local date at the place they describe (a departure date is the date at the departure airport). - Timestamps are ISO 8601 in UTC, for example
2026-11-01T15:18:23Z. A few supplier-derived values come without a zone and are UTC too (the eSIM order and SIMcreated_at,usageexpiry). The one exception iscached_atin the eSIM compatible-devicesmeta, which is in server local time and only tells you how old the cache is. - Flight times in itineraries are the local time of the airport and carry no zone.
Pagination
| Where | Parameters | Answer |
|---|---|---|
| Account history, balance history | page, limit (1 to 100) | pagination: { page, limit, total, hasMore } |
| eSIM bookings | page, limit (1 to 200), search, all | pagination: { total, page, limit, pages } |
| Flight and hotel searches | none | Everything comes back in one answer. Narrow it with the filters of the endpoint. |
Both pagination objects also carry total_pages and has_more, so you can read the same names everywhere. total is the real number of matching rows (for the account history it counts the rows the request can reach, up to a ceiling documented on its page).
Enumerations
Send the spellings the guides show (cabin_class: "economy", booking_type: "issue", type: "adult"). Where a value is not recognised the API answers 400 with a code, instead of silently replacing it by a default (older versions of the flight search did the latter). Responses use the spelling of each endpoint, which is documented with it.
Request ids
Every response carries an X-Request-Id header (the brief hand-over 503 during a deployment is the only exception), and every JSON body a request_id. Send your own X-Request-Id (8 to 64 characters of A-Z a-z 0-9 . _ -) and it is echoed back, which lets you join your logs with ours; otherwise one is generated (req_ and 12 hex characters). Quote it when you contact support.
Flight answers keep the 8-character requestId they always had, and their request_id repeats it. For every other endpoint, and for the meta.request_id of an error, the id is the one in the header. Either finds your request: send the header when you have it, and the body value when you only have the answer.
Safe retries
There is no Idempotency-Key header. Searches, prices and lookups can be repeated freely. Calls that create, pay or cancel must be repeated only after you have read the object back: see Retrying safely.