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

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

The rules below hold for every endpoint of flights, hotels, eSIMs and the account. Learn them once.

## Transport and format

| | |
|---|---|
| **Base URL** | `https://api.fx-port.com/api/v1`. One host for sandbox and live: the key decides ([Authentication](https://fx-port.com/docs/authentication.md)). |
| **Protocol** | HTTPS only. `http://` is redirected with a `301`. |
| **Bodies** | JSON in UTF-8 with `Content-Type: application/json`. The one exception is the ACD document upload, which is `multipart/form-data`. 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. `/esims/packages/` 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](https://fx-port.com/docs/rate-limits.md#stay-comfortably-below-the-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](https://fx-port.com/docs/errors.md#errors-that-do-not-come-from-the-api)). |
| **Health** | `GET https://api.fx-port.com/health` and `/health/ready` need no key and are outside `/api/v1`. |
| **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](https://fx-port.com/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.

~~~bash
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
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
~~~

~~~json
{
  "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:

~~~json
{
  "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:

~~~python
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](https://fx-port.com/docs/legacy-names.md). Machine readers can fetch them from `GET /api/v1/conventions/naming` (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](https://fx-port.com/docs/security.md#enforce-booking-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) and `b2b_price` (what your agency is debited: `final_price` minus `agency_commission`). eSIM catalogues expose the same idea as `balance_debit_amount`.
- In the sandbox no balance is debited and `GET /balances` is empty.

## Dates and times

- Dates are `YYYY-MM-DD` and 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 SIM `created_at`, `usage` expiry). The one exception is `cached_at` in the eSIM compatible-devices `meta`, 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](https://fx-port.com/docs/errors.md#retrying-safely).
