# eSIMs API overview

> Browse the package catalog, place orders, top up existing eSIMs and track usage across 190+ countries.

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

Sell travel data eSIMs across 190+ countries from a single API surface. FX-Port sources packages
from its eSIM supplier network and prices them for your agency in your own currency: you never
call a supplier directly. A package is a data plan for one destination (or one region); an order
creates one eSIM from it; a topup adds a package to an eSIM that already exists.

For a summary of what the API covers, who it is for and how to get access, see the [eSIM API for travel companies](https://fx-port.com/b2b-esims-api).


> [!NOTE]
> **Coverage and pricing grow automatically.** FX-Port continuously onboards more eSIM suppliers,
> and each supplier independently expands its own network of mobile operators over time — both
> add new countries, package sizes, and better rates/connectivity to the catalog. Your integration
> picks up new packages with no code changes — always read `package_id` from the catalog response
> rather than hard-coding it.


## How an integration works


**Step 1: Read the catalog**

[`GET /esims/packages?country=france`](https://fx-port.com/docs/esims/packages.md) returns the packages for a destination with
prices in your currency. Keep the `package_id` of the package the traveler chooses.

**Step 2: Create the order**

[`POST /esims/orders`](https://fx-port.com/docs/esims/orders.md) with that `package_id` and the traveler's email and phone creates one eSIM.
Keep `booking_id` and the ICCID (`data.iccid`) from the response.

**Step 3: Give the traveler the eSIM**

Show `sim.qrcode_url` or the `qrcode` text, or let the traveler open the `sharing.link`.
[Setup instructions](https://fx-port.com/docs/esims/instructions.md) returns the same data again later, with installation steps.

**Step 4: Follow the usage**

[`GET /esims/usage/{iccid}`](https://fx-port.com/docs/esims/usage.md) returns the data left and the expiry.

**Step 5: Top up when the traveler runs out**

[`GET /esims/topup-packages/{iccid}`](https://fx-port.com/docs/esims/topups.md) lists what can be added;
[`POST /esims/topups`](https://fx-port.com/docs/esims/topups.md) adds it.


## eSIM concepts

Read these before integrating.


- [eSIM pricing](https://fx-port.com/docs/esims/pricing.md): What each price field means and how to compute what is debited from your balance.
- [Supplier continuity](https://fx-port.com/docs/esims/supplier-continuity.md): Why a topup can occasionally be unavailable, and how FX-Port protects your balance.


## Endpoints

| Method | Path | Permission | Rate limit group | Description |
|---|---|---|---|---|
| `GET` | `/api/v1/esims/packages` | Read | Default | List the package catalog, filtered by country and type |
| `GET` | `/api/v1/esims/compatible-devices` | Read | Default | List devices known to support eSIM |
| `POST` | `/api/v1/esims/orders` | **Read+Write** | Booking | Create a new eSIM order |
| `GET` | `/api/v1/esims/topup-packages/{iccid}` | Read | Default | List the packages that can top up one eSIM |
| `POST` | `/api/v1/esims/topups` | **Read+Write** | Booking | Top up an existing eSIM |
| `GET` | `/api/v1/esims/usage/{iccid}` | Read | Default | Usage of one eSIM |
| `GET` | `/api/v1/esims/usage/bulk` | Read | Default | Usage of several eSIMs |
| `GET` | `/api/v1/esims/bookings` | Read | Default | List the agency's eSIM orders (topups are not listed here) |
| `GET` | `/api/v1/esims/bookings/{iccid}/topup-history` | Read | Default | Topups of one eSIM |
| `GET` | `/api/v1/esims/bookings/{booking_id}/verify` | Read | Default | Check that a booking is yours |
| `GET` | `/api/v1/esims/bookings/{booking_id}/setup-instructions` | Read | Default | Setup data (QR code, APN, steps) of an order |
| `GET` | `/api/v1/esims/instructions/{iccid}` | Read | Default | Installation instructions for an ICCID |

Only **order creation** and **topup creation** change anything: on live they debit your agency
balance and provision the eSIM. Every other endpoint only reads. The full parameters, schemas and
examples of every endpoint are in the [API reference](https://fx-port.com/docs/api.md).


> [!WARNING]
> `GET /esims/bookings/{booking_id}/setup-url` is **deprecated** and must not be used: it is replaced by
> [setup-instructions](https://fx-port.com/docs/esims/instructions.md#setup-instructions-by-booking), which returns the
> same data as structured JSON. It no longer returns any supplier data: it answers with the install links of the
> order only (see [the setup link](https://fx-port.com/docs/esims/instructions.md#deprecated-the-setup-link)).


## Rate limits

Every API key may send **1,200 requests per minute to the booking group** (creating an order and creating a
topup) and **6,000 requests per minute to the default group** (every other eSIM endpoint: the catalog, devices,
topup packages, usage, bookings, instructions). The counters are per key and per
minute. A request over the limit is answered `429` with the code `rate_limit_exceeded`: see
[Rate limits and fair use](https://fx-port.com/docs/rate-limits.md). Too many orders or topups of one key or agency running at the same time
can be answered `429 concurrency_limit_exceeded`: retry after the seconds in `Retry-After`. Separately, the supplier limits how often usage can be
read: see [eSIM usage](https://fx-port.com/docs/esims/usage.md).

## Sandbox behaviour

A sandbox key (`fxp_test_...`) works against the supplier's test system, so every endpoint above can
be tried end to end:

| Behaviour | Detail |
|---|---|
| **An order creates a test eSIM** | The supplier's sandbox returns a real test eSIM with a dummy activation code (`LPA:1$lpa.airalo.com$DUMMY-...`). It cannot be installed on a phone. |
| **Nothing is debited** | Orders and topups carry `balanceDebited`, `transactionCreated` and `payoutCreated` (twins: `balance_debited`, `transaction_created`, `payout_created`), all `false`. They are also on the error bodies of both endpoints. |
| **Topup balance** | A topup answers `balance` `previous: 0.0` and `current: 0.0`: the sandbox has no balance. |
| **Usage** | A new eSIM shows its full allowance and `ACTIVE`; usage does not change after a topup. |
| **Bookings** | Sandbox orders and topups appear only to sandbox keys: orders in [bookings](https://fx-port.com/docs/esims/bookings.md), topups in the topup history, and both in the [account history](https://fx-port.com/docs/account/history.md) and the [booking detail](https://fx-port.com/docs/account/booking-detail.md). |
| **Live-only checks** | The balance check (`402 insufficient_balance`), the 60-second duplicate check and the balance lock (`503 balance_lock_timeout`) only run on live. |

## Traveler contact details

`email` and `phone` are **required** on [order](https://fx-port.com/docs/esims/orders.md) and [topup](https://fx-port.com/docs/esims/topups.md) creation —
not optional. What happens to them:

- `email`: on an **order**, FX-Port asks the supplier to email the QR code and a sharing link to
  this address as soon as the eSIM is created, so the traveler can install it without you relaying
  the QR code. On a **topup** the email is kept on the booking only; it is not sent to the supplier.
- `phone`, `first_name`, `last_name`: kept on the booking and returned by the booking endpoints. They
  are not sent to the supplier.

Only pass the traveler's own contact details — never billing, pricing, or account information.
`first_name` and `last_name` are optional and are **not restricted to ASCII**: accents and
non-Latin scripts are accepted as they are (only the length is checked).

## Conventions of the eSIM endpoints

- **Bodies have `success`.** Every answer carries `success` (a call that fails also carries the usual error body, see below), except the answer of [verify](https://fx-port.com/docs/esims/bookings.md#check-that-a-booking-is-yours).
- **Creations answer `200`**, not `201`, and have no `Location` header.
- **JSON in, JSON out.** Send `Content-Type: application/json` on orders and topups (the media type is read in any case). A form-encoded body is also read, but a JSON body without the JSON content type is read as an empty form and refused with `missing_field`.
- **Wrapped payloads.** Topup packages, usage and instructions wrap their payload one level deeper (`data.data`); each entry of bulk usage does it again (`data.data.data`). The reference shows each shape.
- **Units and times.** Data is in megabytes. Times that come from the supplier (`created_at` of an order or eSIM, `expired_at` of usage) are `YYYY-MM-DD HH:MM:SS` in UTC without a zone; times stored by FX-Port (bookings) are ISO 8601 with a zone.
- **No trailing slash.** `/api/v1/esims/packages/` is redirected with an empty body: call the path without the slash.
- **Amounts** are in the currency of your agency (`currency`), never in the supplier's.
- **Spellings that are read.** An ICCID may be written in groups of spaces or hyphens (`8944 4654 0000 0123 456`); `locale` takes any spelling of a locale (`zh-Hans`, `pt_BR`, `fr_FR.UTF-8`, an `Accept-Language` value) and `country` a country name (`United States`); an empty `page`, `limit`, `force` or `all` (`?page=`) means not sent. A value that is really wrong keeps its stable code (`invalid_iccid`, `invalid_locale`, `invalid_country` ...).
- **Names.** Several answers keep a legacy camelCase or short key next to its snake_case twin (`code` and `order_id`, `id` and `package_id`, `balanceDebited` and `balance_debited`, `pagination.pages` and `total_pages` ...). Both carry the same value; use the snake_case one in new code. `?naming=snake` returns every key in snake_case. See [Conventions and naming](https://fx-port.com/docs/conventions.md).

## Error responses

Every error has a stable `code` in `errors[0].code`: branch on it, not on the message. See
[Errors](https://fx-port.com/docs/errors.md) for the shape and every shared code.

| HTTP | Typical `code` | Cause |
|---|---|---|
| `400` | `missing_field`, `invalid_package_id`, `invalid_iccid`, `invalid_email`, `invalid_phone`, `invalid_type`, `invalid_json` | A required field is missing or has the wrong format |
| `401` | `authentication_required`, `invalid_api_key` | Missing or invalid API key |
| `402` | `insufficient_balance` | Agency balance lower than the amount to debit (live) |
| `403` | `read_write_permission_required`, `agency_not_active` | The key has read permission only, or the agency is not active |
| `404` | `not_found`, `iccid_not_found`, `booking_not_found` | The package, the eSIM or the booking does not exist for your agency |
| `409` | `duplicate_request` | The same order or topup is already being processed or was just confirmed |
| `422` | `validation_error` | A query parameter has the wrong type or range (for example `limit=500`) |
| `429` | `rate_limit_exceeded`, `concurrency_limit_exceeded` | Too many requests per minute, or too many orders or topups of your key or agency being processed at the same time. Retry after the seconds in `Retry-After` |
| `500` | `internal_error`, `booking_record_failed`, `balance_debit_failed`, `supplier_response_invalid` | An error on our side; the last three come after the supplier created the eSIM |
| `503` | `service_unavailable`, `balance_lock_timeout`, `duplicate_check_unavailable`, `service_busy` | The supplier connection, a lock or the service is temporarily unavailable; retry in a few seconds |

When the **supplier** refuses an order or a topup, the response is a `supplier_error` with the supplier's own
HTTP status (for example `422` or `429`), nothing is debited, and a `failed` booking is saved for
support. Usage, topup packages, topups and instructions answer `404 iccid_not_found` for an ICCID that is not
an eSIM of your agency; setup instructions answer `404 booking_not_found` for a booking that is not yours.

**Loading answers.** Right after a restart the catalog and the device list may not be loaded yet:
`GET /esims/packages` and `GET /esims/compatible-devices` then answer **`202`** with
`"success": false` and an empty `data`, and the device list can answer `503` while its fetcher starts.
This is not an error of your request: wait a few seconds and send it again.

## Questions?

Email the FX-Port developer team at [dev@fx-port.com](mailto:dev@fx-port.com) for integration
help, feature requests, or to report an API error.
