Skip to content
FX-Port/Docs
GuidesAPI referenceChangelog
Get API keys
  • Introduction
  • Quickstart
  • Security
  • Authentication
  • Environments and Postman
  • Sandbox
  • Conventions and naming
  • Legacy names and twins
  • Errors
  • Rate limits and fair use
  • Streaming search results

Concepts

  • Overview
  • Pricing
  • Supplier continuity

Catalog and orders

  • Package catalog
  • Create an order
  • Top up

After the order

  • Usage
  • Bookings
  • Setup instructions
  • Use these docs with AI

eSIMs ยท Catalog and orders

Top up an eSIM

Check topup availability and top up an existing eSIM by ICCID. Creation requires a read+write API key.

Topping up is a two-step flow: first ask which packages can top up this exact eSIM, then create the topup with one of the returned package_id values. Always follow this order: see Supplier continuity for why.

1. Check available topup packages

GET /api/v1/esims/topup-packages/{iccid}

Permission: Read

Lists the packages that can be added to one eSIM, with prices in your agency currency. The answer is scoped to the ICCID, so it reflects what the supplier offers for this eSIM today, including the rare case where the network operator of the country changed since the eSIM was issued.

The ICCID must belong to an eSIM that your agency ordered in the same environment: 18 to 22 digits, which may be written in groups of spaces or hyphens (8944 4654 0000 0123 456; the digits are read). An ICCID that is unknown, or that belongs to another agency, is answered 404 iccid_not_found before anything is asked from the supplier. A sandbox key only sees sandbox eSIMs.

curl --request GET \
  --url 'https://api.fx-port.com/api/v1/esims/topup-packages/8900000540463077957?locale=en' \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY'
const iccid = "8900000540463077957";
const response = await fetch(`https://api.fx-port.com/api/v1/esims/topup-packages/${iccid}?locale=en`, {
  headers: { Authorization: "Bearer fxp_test_YOUR_KEY" },
});
const body = await response.json();
const packages = body.data.data; // the list is one level deeper than the other catalogs
import requests

iccid = "8900000540463077957"
response = requests.get(
    f"https://api.fx-port.com/api/v1/esims/topup-packages/{iccid}",
    params={"locale": "en"},
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    timeout=30,
)
packages = response.json()["data"]["data"]
Query parameterDescription
localeLanguage of the text fields: en, fr, ar, de, es, it, ja, ko, pl, pt, ro, ru, sq, tr, vi or zh. Any spelling of a locale is read and only its language counts (fr-CA, zh-Hans, pt_BR, fr_FR.UTF-8, or a whole Accept-Language value such as fr-FR,fr;q=0.9: the first language is used). Another well-formed language is answered in English with a coerced_value warning; a value that is not a locale is refused with invalid_locale. Falls back to Accept-Language, then en
forcetrue skips the cache and asks the supplier again. An empty value means not sent

Caching. The list is kept for 5 minutes per eSIM and language. A cached answer carries cached: true and cache_age_seconds; a fresh one has neither. Use force=true only when you need the very latest list.

Response example

A sandbox answer (the prices block shortened):

{
  "environment": "sandbox",
  "success": true,
  "data": {
    "data": [
      {
        "id": "france-in-7days-1gb-topup",
        "package_id": "france-in-7days-1gb-topup",
        "type": "topup",
        "price": 468.84,
        "amount": 1024,
        "day": 7,
        "is_unlimited": false,
        "title": "1 GB - 7 days",
        "data": "1 GB",
        "short_info": null,
        "voice": null,
        "text": null,
        "prices": { "recommended_retail_price": { "EUR": 3.56, "GBP": 3, "USD": 4 } },
        "base_price": 402.5,
        "commission": 50.24,
        "total_price": 468.84,
        "retail_price": 468.84,
        "balance_debit_amount": 418.6,
        "b2b_price": 418.6,
        "agency_commission": "12 %",
        "commission_rate": 12,
        "currency": "DZD",
        "supplier_currency": "USD"
      }
    ]
  }
}

An empty data.data array means this eSIM cannot be topped up now: place a new eSIM order for the traveler instead.

Package fields

FieldTypeMeaning
package_idstringThe id to send as package_id when you create the topup. id carries the same value (legacy name)
typestringAlways topup
title, data, amount, dayLabel, data allowance as text, allowance in MB (0 is unlimited) and validity in days
is_unlimited, voice, text, short_infoUnlimited flag, voice minutes and SMS (null for data only) and a short note (usually null)
pricenumberThe retail price: the same amount as total_price. It is not the catalog's base price
base_pricenumberThe base price before your commission: the amount the catalog calls price
commissionnumberYour commission as an amount
total_price / retail_pricenumberThe price to charge the traveler. Both fields have the same value
balance_debit_amountnumberThe amount debited from your agency balance by the topup. b2b_price carries the same value
agency_commission / commission_ratestring / numberYour commission rate as text ("12 %") and as a number (12; 0 for contracted corporate agencies)
currency, supplier_currencystringYour agency currency (the currency of every amount) and the supplier's currency
prices.recommended_retail_priceobjectThe supplier's recommended retail price by currency, for reference only

How the amounts relate is explained in eSIM pricing.

2. Create the topup

POST /api/v1/esims/topups

Permission: Read+Write

Send one package_id from step 1 and the ICCID you asked about. Prices are computed on the server; a price in the body is ignored.

curl --request POST \
  --url https://api.fx-port.com/api/v1/esims/topups \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "package_id": "france-in-7days-1gb-topup",
    "iccid": "8900000540463077957",
    "first_name": "John",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone": "+15555550123"
  }'
const response = await fetch("https://api.fx-port.com/api/v1/esims/topups", {
  method: "POST",
  headers: { Authorization: "Bearer fxp_test_YOUR_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    package_id: "france-in-7days-1gb-topup",
    iccid: "8900000540463077957",
    email: "[email protected]",
    phone: "+15555550123",
  }),
});
const body = await response.json();
if (!response.ok) throw new Error(body.errors?.[0]?.code ?? `HTTP ${response.status}`);
console.log(body.booking_id, body.data.balance);
import requests

response = requests.post(
    "https://api.fx-port.com/api/v1/esims/topups",
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    json={
        "package_id": "france-in-7days-1gb-topup",
        "iccid": "8900000540463077957",
        "email": "[email protected]",
        "phone": "+15555550123",
    },
    timeout=170,
)
body = response.json()
response.raise_for_status()

Body fields

FieldTypeRequiredDescription
package_idstringYesA package_id returned by topup-packages for this ICCID. 1 to 128 characters: letters, digits, -, _, .
iccidstringYesThe eSIM to top up: 18 to 22 digits (groups of spaces or hyphens are read), an eSIM your agency ordered
emailstringYesThe traveler's email address, at most 254 characters. It is kept on the booking only: on a topup it is not sent to the supplier, so the supplier does not email anything to the traveler
phonestringYesThe traveler's phone number: at least 3 digits, at most 32 characters. Kept on the booking
first_name / last_namestringNoTraveler name, at most 100 characters. Any characters are accepted, accents and non-Latin scripts included
agent_idstring (UUID)NoThe user of your agency who places the topup. It must be a user of your agency (invalid_agent_id otherwise). Most integrations omit it

Legacy names (still work; when both spellings are sent, the new one wins):

LegacyUse insteadNotes
user_idagent_idThe response carries a deprecated_field warning
customer_email, customer_phone, customer_first_name, customer_last_nameemail, phone, first_name, last_nameThe names the booking rows use. Accepted without a warning

Send Content-Type: application/json (the media type is read in any case). Keys the endpoint does not read, quantity and every price field included, are ignored and listed in the warnings of the response as ignored_field.

Which package may I send?

The topup is priced from the list that step 1 returned for this ICCID, so package_id must come from that list. A package that is not in it is refused with 404 not_found ("Package ... not found or has no valid price"), and nothing is debited. Should you get that answer for a package you have just received (for example right after FX-Port was updated), ask for the list again with force=true and send the topup again.

Response example

A sandbox topup. The response has status 200, not 201.

{
  "environment": "sandbox",
  "success": true,
  "data": {
    "topup": {
      "code": "20261101-106853",
      "order_id": "20261101-106853",
      "package_id": "france-in-7days-1gb-topup",
      "package": "KPN France Data only-1 GB - 7 days",
      "data": "1 GB",
      "validity": 7,
      "price": 468.84,
      "currency": "DZD",
      "total_price": 468.84,
      "retail_price": 468.84,
      "agency_currency": "DZD",
      "supplier_currency": "USD",
      "exchange_rate": 250.0
    },
    "iccid": "8900000540463077957",
    "balance": { "previous": 0.0, "current": 0.0, "currency": "DZD" }
  },
  "booking_id": "FX-1793546625391",
  "message": "eSIM topup completed successfully",
  "balanceDebited": false,
  "transactionCreated": false,
  "payoutCreated": false,
  "balance_debited": false,
  "transaction_created": false,
  "payout_created": false
}
FieldMeaning
data.topup.codeThe supplier's order code of the topup. order_id carries the same value
data.topup.price / total_price / retail_priceWhat the traveler pays: the retail price, in your agency currency
data.topup.package_id, package, data, validityThe package that was bought: id, name, allowance and validity in days
data.iccidThe eSIM that was topped up
data.balanceYour agency balance in this currency before (previous) and after (current) the debit
booking_idThe booking of the topup (FX- followed by a number). It is listed by topup history and by the account history, not by the eSIM bookings list
balanceDebited, transactionCreated, payoutCreatedSandbox only, always false. Twins: balance_debited, transaction_created, payout_created

In the sandbox

A sandbox topup is accepted by the supplier's test system and debits nothing: balance is 0.0 before and after, and the three flags are false. The usage of a sandbox eSIM does not change after a topup. The balance check and the duplicate check run on live only.

Does a topup extend or replace the current plan?

A topup adds the package's data allowance to the eSIM: it does not replace or shorten what the traveler already has. How the validity changes depends on the package, so FX-Port does not predict it. Read the result from eSIM usage after the topup (remaining, total and expired_at), with force=true, because usage is cached for 15 minutes.

Duplicates and retries

There is no idempotency key. A second topup is refused with 409 duplicate_request while an identical one (same eSIM and package) is being processed, and on live also when a topup for the same eSIM was created in the last 60 seconds, whatever the package (existing_booking_id names it). If a call times out, look at topup history before you send it again.

Errors

HTTPcodeCause
400missing_fieldpackage_id, iccid, email or phone is missing
400invalid_package_id, invalid_iccid, invalid_email, invalid_phone, invalid_name, invalid_agent_idA value has the wrong format or length
400invalid_json, invalid_bodyThe body is not valid JSON, or is not a JSON object
401authentication_required, invalid_api_keyMissing or invalid API key
402insufficient_balanceAgency balance lower than the amount to debit (live). details has available, required and currency
403read_write_permission_requiredThe key has read permission only
403agency_not_activeThe agency is not active for this environment (live)
404iccid_not_foundThe ICCID is not an eSIM of your agency in this environment
404not_foundThe package is not a topup package of this eSIM (see Which package may I send?)
409duplicate_requestAn identical topup is being processed, or a topup of this eSIM was just confirmed (live)
429rate_limit_exceededOver 1,200 requests per minute for this key
429concurrency_limit_exceededToo many topups of your key or agency are being processed at the same time. Nothing was topped up: retry after the seconds in Retry-After
supplier statussupplier_errorThe supplier refused the topup, for example because the operator no longer offers the package. The status is the supplier's, nothing was debited, and the body has the booking_id of the failed booking
500booking_record_failed, balance_debit_failed, supplier_response_invalidThe supplier accepted the topup but FX-Port could not save it or take the debit, or the supplier's answer was unusable. Do not retry: contact support with the booking_id in the body
503service_unavailable, balance_lock_timeout, duplicate_check_unavailable, service_busyThe supplier connection, the balance lock, the duplicate check or the service is busy: nothing was topped up. Retry in a few seconds

Step 1 answers 400 invalid_iccid or invalid_locale, 404 iccid_not_found, and 503 service_unavailable when the supplier cannot be reached. When the supplier refuses the lookup, the answer has the supplier's status (for example 429).

If the supplier rejects the topup for any reason, including an operator change in the destination country, your agency balance is never debited. The attempt is recorded as a failed booking for support and the response is a supplier_error so you can react immediately.

PreviousCreate an eSIM orderNext eSIM usage

On this page

  • 1. Check available topup packages
  • Response example
  • Package fields
  • 2. Create the topup
  • Body fields
  • Which package may I send?
  • Response example
  • In the sandbox
  • Does a topup extend or replace the current plan?
  • Duplicates and retries
  • Errors

ยฉ 2026 FX-Port

TermsPrivacy[email protected]