# Booking history

> Booking summaries across all products, newest first.

Endpoint: `GET /api/v1/history`

Source: https://fx-port.com/docs/account/history

`GET /api/v1/history`

**Permission:** Read

Booking summaries across one or all products (flights, hotels, eSIMs and transfers), newest first, limited to the
environment of your key. eSIM orders **and** topups are listed (`type` tells them apart).


> [!NOTE]
> The records are lightweight: enough for status tracking, reconciliation and dashboards. For one booking in full, use
> the [booking detail](https://fx-port.com/docs/account/booking-detail.md) (`GET /api/v1/bookings/{booking_id}`) or the product's own
> endpoint (for example `POST /api/v1/flights/get_booking`).


## Query parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `product` | string | `all` | `all`, `flights`, `hotels`, `esims` or `transfers`. Case does not matter and the singular names (`flight`, `hotel`, `esim`, `transfer`) are read as the plural. Anything else is `400 invalid_product` |
| `status` | string | none | Keep bookings with exactly this status, for example `confirmed`. Case does not matter; spaces and hyphens are read as underscores (`On Hold` is `on_hold`); letters, digits and `_`, at most 32 characters. `pending` is accepted as another name of `awaiting_payment`. A status nothing has returns an empty list |
| `date_from` | string | none | Inclusive start, on the creation time of the booking. Any ISO 8601 spelling is read: a date (`2026-07-01` or `20260701`, 00:00:00 UTC) or a datetime with 1 to 9 fraction digits and an offset written `Z`, `+01:00` or `+0100` (in a URL write the `+` as `%2B`). Without an offset a datetime is UTC |
| `date_to` | string | none | Inclusive end, in the same spellings. A bare date means the end of that day (23:59:59.999999 UTC). Not before `date_from` |
| `page` | integer | `1` | The page, starting at 1 (at most 10,000). An empty value means not sent |
| `limit` | integer | `20` | Entries per page, 1 to **100** (not 200). A larger value is refused with `422 validation_error`; an empty value means not sent |

**Legacy names** (still work; the response carries a `deprecated_field` warning; `date_from` and `date_to` win when both are sent):

| Legacy | Use instead |
|---|---|
| `from`, `from_date` | `date_from` |
| `to`, `to_date` | `date_to` |

Never send `agency_id`: an API key that does is refused with `400 agency_id_not_allowed`.

### Pagination

`pagination` is `{ page, limit, total, total_pages, hasMore, has_more }`. `total` is the **real number** of bookings
that match (all products together). The pages are served from the **200 newest bookings of each product**, so
with `product=all` at most 800 bookings can be reached by paging: follow `has_more` (`hasMore` is the same value), and
use `date_from` and `date_to` to read an older period.

## Request examples


**cURL**

~~~bash
# The latest eSIM bookings
curl --get \
  --url https://api.fx-port.com/api/v1/history \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY' \
  --data-urlencode 'product=esims' \
  --data-urlencode 'limit=2'

# Confirmed flights of a period
curl --get \
  --url https://api.fx-port.com/api/v1/history \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY' \
  --data-urlencode 'product=flights' \
  --data-urlencode 'status=confirmed' \
  --data-urlencode 'date_from=2026-11-01' \
  --data-urlencode 'date_to=2026-11-30'
~~~

**Node.js**

~~~js
const params = new URLSearchParams({ product: "flights", status: "confirmed", date_from: "2026-11-01", limit: "100" });
const response = await fetch(`https://api.fx-port.com/api/v1/history?${params}`, {
  headers: { Authorization: "Bearer fxp_test_YOUR_KEY" },
});
const { data: bookings, pagination } = await response.json();
// keep the internal id as your key; bookingId is a display reference
const ids = bookings.map((booking) => booking.id);
~~~

**Python**

~~~python
import requests

response = requests.get(
    "https://api.fx-port.com/api/v1/history",
    params={"product": "esims", "limit": 50, "date_from": "2026-11-01"},
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    timeout=30,
)
body = response.json()
~~~


## Response example

An eSIM order, as the API returns it (a sandbox answer of the request above):

~~~json
{
  "environment": "sandbox",
  "success": true,
  "data": [
    {
      "product": "esims",
      "id": "639417b4-b103-4ece-bd5d-850315322278",
      "bookingId": "FX-1793546602916",
      "booking_id": "FX-1793546602916",
      "status": "confirmed",
      "environment": "sandbox",
      "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
      "agency_id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
      "agentId": null,
      "agent_id": null,
      "currency": "DZD",
      "totalPrice": 468.84,
      "total_price": 468.84,
      "netPrice": 418.6,
      "b2b_price": 418.6,
      "agencyCommission": 50.24,
      "agency_commission": 50.24,
      "providerName": "Airalo",
      "provider_name": "Airalo",
      "supplierId": "airalo",
      "supplier_id": "airalo",
      "createdAt": "2026-11-01T15:23:22.961228+00:00",
      "created_at": "2026-11-01T15:23:22.961228+00:00",
      "updatedAt": "2026-11-01T15:23:22.961228+00:00",
      "updated_at": "2026-11-01T15:23:22.961228+00:00",
      "orderId": "20261101-106852",
      "order_id": "20261101-106852",
      "type": "esim",
      "packageName": "1 GB - 7 days • France",
      "package_name": "1 GB - 7 days • France",
      "iccid": "8900000540463077957",
      "customerEmail": "traveler@example.com",
      "customer_email": "traveler@example.com",
      "customer_first_name": "John",
      "customer_last_name": "Smith",
      "customer_phone": "+15555550123"
    }
  ],
  "pagination": { "page": 1, "limit": 2, "total": 80, "total_pages": 40, "hasMore": true, "has_more": true },
  "meta": {
    "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
    "agency_id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
    "environment": "sandbox",
    "product": "esims"
  }
}
~~~

The entries of the other products have the same first fifteen keys and their own keys after `updatedAt`. They are shown
without their snake_case twins to keep them short:


**Flights**

~~~json
{
  "product": "flights",
  "id": "8723790a-0417-46bb-8247-07254b214a4c",
  "bookingId": "Y7INFJ",
  "status": "awaiting_payment",
  "environment": "sandbox",
  "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "agentId": null,
  "currency": "DZD",
  "totalPrice": 32593.93,
  "netPrice": 32561.36,
  "agencyCommission": 32.57,
  "providerName": "Amadeus Algeria",
  "supplierId": "amadeus_aqc_dz_1",
  "createdAt": "2026-11-01T14:31:20.248661+00:00",
  "updatedAt": "2026-11-01T14:31:22.611902+00:00",
  "departureAirport": "ALG",
  "arrivalAirport": "CDG",
  "departureDate": "2026-12-15T07:15:00",
  "arrivalDate": "2026-12-15T09:45:00",
  "ticketNumber": null,
  "requireAcd": false,
  "voucherUrl": "https://files.example.com/flight-bookings/Y7INFJ-JOHN-SMITH.pdf",
  "voucherUrlWithPrice": "https://files.example.com/flight-bookings/Y7INFJ-JOHN-SMITH.pdf",
  "voucherUrlNoPrice": "https://files.example.com/flight-bookings/Y7INFJ-JOHN-SMITH-no-price.pdf"
}
~~~

**Hotels**

~~~json
{
  "product": "hotels",
  "id": "34a08cf3-dff6-45c6-bdae-882061aa78f5",
  "bookingId": "HMVXX4ZQ3",
  "status": "refunded",
  "environment": "sandbox",
  "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "agentId": null,
  "currency": "DZD",
  "totalPrice": 1484.97,
  "netPrice": 1482.0,
  "agencyCommission": 2.97,
  "providerName": "ETG - RateHawk",
  "supplierId": "ratehawk",
  "createdAt": "2026-11-01T14:22:00.924125+00:00",
  "updatedAt": "2026-11-01T14:22:32.992144+00:00",
  "hotelId": "test_hotel_do_not_book",
  "hotelName": "Test Hotel (Do Not Book) test",
  "checkinDate": "2026-12-01",
  "checkoutDate": "2026-12-04",
  "bookingMode": "standard",
  "voucherUrl": "https://files.example.com/hotel-bookings/HMVXX4ZQ3-Jane-Smith.pdf",
  "voucherUrlWithPrice": "https://files.example.com/hotel-bookings/HMVXX4ZQ3-Jane-Smith.pdf",
  "voucherUrlNoPrice": "https://files.example.com/hotel-bookings/HMVXX4ZQ3-Jane-Smith-no-price.pdf"
}
~~~

**Transfers**

~~~json
{
  "product": "transfers",
  "id": "4745c761-0737-421a-aaab-c90f9c0aca8f",
  "bookingId": "VIA-1782555657-QCF",
  "status": "refunded",
  "environment": "sandbox",
  "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "agentId": "5d6e7f80-91a2-4b3c-8d4e-5f6071829304",
  "currency": "DZD",
  "totalPrice": 15124.38,
  "netPrice": 15002.38,
  "agencyCommission": 122.0,
  "providerName": "AirportTransfer.com",
  "supplierId": null,
  "createdAt": "2026-06-27T10:20:59.188372+00:00",
  "updatedAt": "2026-06-27T10:29:28.684+00:00",
  "arrivalDate": "2026-08-14T16:00:00+00:00",
  "flightNumber": "593",
  "voucherUrl": null,
  "receiptUrl": "https://files.example.com/transfer-bookings/VIA-1782555657-QCF.pdf"
}
~~~


## Summary fields

**Every product**

| Field (twin) | Description |
|---|---|
| `product` | `flights`, `hotels`, `esims` or `transfers` |
| `id` | The internal id of the booking (UUID). Stable and unique: **use it as your key** |
| `bookingId` (`booking_id`) | The public reference: the airline record locator of a flight (**not unique** across suppliers), the hotel booking id, `FX-` followed by a number for an eSIM, or the transfer reference |
| `status` | The state of the booking (see below) |
| `environment` | `sandbox` or `live` |
| `agencyId` (`agency_id`) | Your agency UUID |
| `agentId` (`agent_id`) | The user of your agency who created the booking, or `null` when it was created through an API key without a user |
| `currency` | Your agency currency: the currency of every amount |
| `totalPrice` (`total_price`) | What the traveler pays, with your commission |
| `agencyCommission` (`agency_commission`) | Your commission as an **amount**. `0` when there is none |
| `netPrice` (`b2b_price`) | The amount **debited from your balance**: `totalPrice` minus `agencyCommission` |
| `providerName` (`provider_name`) | The supplier that fulfilled the booking |
| `supplierId` (`supplier_id`) | The supplier id stored on the booking (see the note below) |
| `createdAt` (`created_at`), `updatedAt` (`updated_at`) | ISO 8601, UTC |

**Flights**: `departureAirport`, `arrivalAirport` (IATA), `departureDate`, `arrivalDate` (local, without a zone), `ticketNumber`
(`null` until issued), `requireAcd` (`require_acd`), `voucherUrl` and `voucherUrlWithPrice` (the confirmation PDF with prices) and
`voucherUrlNoPrice` (without prices, to give to the traveler). The voucher links are `null` while the PDF is generated.

**Hotels**: `hotelId`, `hotelName` (can be `null` on a failed booking), `checkinDate`, `checkoutDate` (`YYYY-MM-DD`), `bookingMode`, and
the same three voucher links.

**eSIMs**: `orderId` (`order_id`, the supplier's order code), `type` (`esim` or `topup`), `packageName` (`package_name`), `iccid`,
`customerEmail` (`customer_email`), `customer_first_name`, `customer_last_name`, `customer_phone`.

**Transfers**: `arrivalDate`, `flightNumber`, `voucherUrl` (the supplier voucher: `null` until a driver is assigned) and `receiptUrl`
(FX-Port's receipt PDF, once the booking is confirmed).


> [!NOTE]
> `supplierId` is the id stored on the booking: `amadeus_aqc_dz_1` and `duffel_1` for flights, but `ratehawk` for
> hotels and `airalo` for eSIMs, while the [supplier list](https://fx-port.com/docs/suppliers.md) calls the same suppliers `worldota_1` and
> `airalo_1`. Do not match the two lists on the id.


## Status values

| Status | Products | Meaning |
|---|---|---|
| `confirmed` | All | The booking is confirmed |
| `awaiting_payment` | Flights | A held booking that is not paid yet. `pending` is accepted as another name in the filter |
| `cancelled` | Flights | The booking was cancelled |
| `failed` | Flights, eSIMs | The supplier refused the booking |
| `refunded` | Flights, hotels, transfers | The booking was refunded |
| `partial_refund` | Flights | Part of the booking was refunded |

These are the values seen today; a product can use others, and a filter on a status nothing has answers an empty list.

## Errors

| HTTP | `code` | Cause |
|---|---|---|
| `400` | `invalid_product` | `product` is not one of the allowed values (`details.allowed` lists them) |
| `400` | `invalid_status` | `status` is not a plain status name (letters, digits, `_`, spaces or hyphens, at most 32 characters) |
| `400` | `invalid_date`, `invalid_date_range` | A date is not valid, or `date_from` is after `date_to` |
| `400` | `agency_id_not_allowed` | An API key sent `agency_id` |
| `401` | `authentication_required`, `invalid_api_key` | Missing or invalid API key |
| `422` | `validation_error` | `page` is below 1 or above 10,000, or `limit` is above 100 |
| `429` | `rate_limit_exceeded` | Over 6,000 requests per minute for this key |
| `503` | `service_overloaded` | The service is shedding load for a moment (its database pool is full). Nothing was read: retry after the seconds in `Retry-After` (2 to 5) |
