# eSIM bookings

> List the eSIM orders of your agency, read the topups of an eSIM and check that a booking is yours.

Source: https://fx-port.com/docs/esims/bookings

Three read-only endpoints cover the eSIM orders of your agency: the list of orders, the topups of one eSIM,
and a check that a booking reference is yours. Orders and topups are stored as bookings of the same shape;
the list shows **orders only**, and the topups of an eSIM are read with the topup history.

## List bookings

`GET /api/v1/esims/bookings`

**Permission:** Read

A paginated list of the **eSIM orders** of your agency in this environment, newest first. A sandbox key lists
sandbox orders and a live key lists live orders. **Topups are not in this list**: read them with the
[topup history](#topup-history-for-an-iccid) of the eSIM, or in the [account history](https://fx-port.com/docs/account/history.md),
which lists orders and topups together. A failed attempt is listed too, with the status `failed`.


**cURL**

~~~bash
curl --request GET \
  --url 'https://api.fx-port.com/api/v1/esims/bookings?page=1&limit=50&search=traveler@example.com' \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY'
~~~

**Node.js**

~~~js
const params = new URLSearchParams({ page: "1", limit: "50", search: "traveler@example.com" });
const response = await fetch(`https://api.fx-port.com/api/v1/esims/bookings?${params}`, {
  headers: { Authorization: "Bearer fxp_test_YOUR_KEY" },
});
const { data: bookings, pagination } = await response.json();
~~~

**Python**

~~~python
import requests

response = requests.get(
    "https://api.fx-port.com/api/v1/esims/bookings",
    params={"page": 1, "limit": 50, "search": "traveler@example.com"},
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    timeout=30,
)
body = response.json()
~~~


| Query parameter | Description |
|---|---|
| `search` | Part of a booking id, the traveler's first or last name, the email, the package name or the ICCID. It does not match the supplier order code or the phone number. At most 100 characters |
| `page` | The page, starting at 1 (at most 10,000) |
| `limit` | Bookings per page, 1 to **200** (default 50). A larger value is refused with `422 validation_error` |
| `all` | `true` returns the largest pages: 200 bookings, see below |

An empty `page`, `limit` or `all` (`?page=`) means not sent and takes the default.

**`search` is safe with any text.** Characters other than letters, digits and `@ . - _ + ' : / & •` are treated as
spaces, so brackets, commas, quotes or wildcards cannot break the search. A term longer than 100 characters, or one
with control characters, is refused with `400 invalid_search`; a term with nothing searchable in it answers an
empty page. `pagination.total` counts the orders that match, `search` included.

**`all=true`** does not return every booking in one answer: an API key gets pages of at most 200 bookings
(`limit` defaults to 200). The response carries a `coerced_value` warning. Follow `pagination.has_more` and
`page` to read the rest.

### Response example

~~~json
{
  "environment": "sandbox",
  "success": true,
  "data": [
    {
      "id": "639417b4-b103-4ece-bd5d-850315322278",
      "booking_id": "FX-1793546602916",
      "order_id": "20261101-106852",
      "status": "confirmed",
      "currency": "DZD",
      "total_price": 468.84,
      "agency_commission": 50.24,
      "type": "esim",
      "package_name": "1 GB - 7 days • France",
      "iccid": "8900000540463077957",
      "customer_email": "traveler@example.com",
      "customer_first_name": "John",
      "customer_last_name": "Smith",
      "customer_phone": "+15555550123",
      "provider_name": "Airalo",
      "supplier_id": "airalo",
      "created_at": "2026-11-01T15:23:22.961228+00:00",
      "updated_at": "2026-11-01T15:23:22.961228+00:00",
      "environment": "sandbox"
    }
  ],
  "pagination": { "total": 61, "page": 1, "limit": 1, "pages": 61, "total_pages": 61, "has_more": true }
}
~~~

### Booking fields

| Field | Meaning |
|---|---|
| `id` | Internal id of the booking row (UUID): the `id` the account history returns |
| `booking_id` | The FX-Port reference, `FX-` followed by a number. Use it in [setup instructions](https://fx-port.com/docs/esims/instructions.md#setup-instructions-by-booking) and in the [booking detail](https://fx-port.com/docs/account/booking-detail.md) |
| `order_id` | The supplier's order code (`code` in the order response) |
| `status` | `confirmed`, or `failed` when the supplier refused the order (the ICCID of such a row starts with `PENDING-`) |
| `type` | `esim` for an order, `topup` for a topup |
| `currency`, `total_price` | Your agency currency and the price charged to the traveler |
| `agency_commission` | Your commission as an **amount**. The amount debited from your balance is `total_price` minus this amount (see [eSIM pricing](https://fx-port.com/docs/esims/pricing.md)) |
| `package_name` | Package name, with the destination for an order |
| `iccid` | The ICCID of the eSIM. A topup row carries the ICCID that was topped up |
| `customer_email`, `customer_first_name`, `customer_last_name`, `customer_phone` | The traveler details you sent when ordering |
| `provider_name`, `supplier_id` | The supplier that fulfilled the booking. Here `airalo`: the [supplier list](https://fx-port.com/docs/suppliers.md) calls the same supplier `airalo_1` |
| `created_at`, `updated_at` | ISO 8601 with a zone (UTC) |
| `environment` | `sandbox` or `live` |

Only agency-facing fields are returned: the supplier's cost and FX-Port's internal margins are never part of a
booking.

### Pagination

| Field | Meaning |
|---|---|
| `total` | Number of orders that match (`search` included) |
| `page`, `limit` | The page returned and the page size used |
| `pages` | Number of pages, `0` when there is no order. `total_pages` carries the same value |
| `has_more` | `true` when another page follows |

## Topup history for an ICCID

`GET /api/v1/esims/bookings/{iccid}/topup-history`

**Permission:** Read

The topups of one eSIM, newest first, with the same booking shape as above and `type: "topup"`. There is no
pagination. The list is **empty** (status `200`) when the eSIM has no topup or is not an eSIM of your agency.

~~~bash
curl --request GET \
  --url https://api.fx-port.com/api/v1/esims/bookings/8900000540463077957/topup-history \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY'
~~~

~~~json
{
  "environment": "sandbox",
  "success": true,
  "data": [
    {
      "id": "bd6ff7d7-392f-4d56-8dea-4f0f3dff0af9",
      "booking_id": "FX-1793546625391",
      "order_id": "20261101-106853",
      "status": "confirmed",
      "currency": "DZD",
      "total_price": 468.84,
      "agency_commission": 50.24,
      "type": "topup",
      "package_name": "1 GB - 7 days",
      "iccid": "8900000540463077957",
      "customer_email": "traveler@example.com",
      "customer_first_name": "John",
      "customer_last_name": "Smith",
      "customer_phone": "+15555550123",
      "provider_name": "Airalo",
      "supplier_id": "airalo",
      "created_at": "2026-11-01T15:23:45.424526+00:00",
      "updated_at": "2026-11-01T15:23:45.424526+00:00",
      "environment": "sandbox"
    }
  ]
}
~~~

The ICCID is 18 to 22 digits (groups of spaces or hyphens are read as the digits), or the `PENDING-...` placeholder of an
order whose eSIM the supplier has not returned yet (letters, digits and `-`, at most 64 characters); anything else is
refused with `400 invalid_iccid`.

## Check that a booking is yours

`GET /api/v1/esims/bookings/{booking_id}/verify`

**Permission:** Read

Tells whether an eSIM booking belongs to your agency, before you build a link or a screen around a `booking_id`.
The reference is the `booking_id` of an eSIM order or topup (`FX-` followed by a number, not the internal UUID), at most 100 characters of letters,
digits and `._:=+~-` (`400 invalid_booking_id` otherwise).

~~~bash
curl --request GET \
  --url https://api.fx-port.com/api/v1/esims/bookings/FX-1793546602916/verify \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY'
~~~

~~~json
{
  "environment": "sandbox",
  "isOwner": true,
  "is_owner": true,
  "userAgencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
  "bookingAgencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d"
}
~~~

| Field | Meaning |
|---|---|
| `isOwner` | `true` when the booking is an eSIM booking of your agency in this environment. `is_owner` carries the same value |
| `userAgencyId`, `bookingAgencyId` | Present only when `isOwner` is `true`; both are your own agency id |

A booking of **another agency** and a booking that **does not exist** are answered the same way, `{"isOwner": false}`
with status `200`, so the call cannot be used to discover which references exist. This answer has no `success` key.

## Errors

| HTTP | `code` | Cause |
|---|---|---|
| `400` | `invalid_search` | `search` is longer than 100 characters or has control characters |
| `400` | `invalid_iccid` | The ICCID of the topup history is not valid |
| `400` | `invalid_booking_id` | The booking reference of `verify` is not valid |
| `401` | `authentication_required`, `invalid_api_key` | Missing or invalid API key |
| `422` | `validation_error` | `page` or `limit` is not an integer in its range (for example `limit=201`) |
| `429` | `rate_limit_exceeded` | Over 6,000 requests per minute for this key |
