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

Create an eSIM order

Create a new eSIM order. Requires a read+write API key.

POST /api/v1/esims/orders

Permission: Read+Write

Creates one eSIM, debits your agency balance (live only) and provisions the eSIM with the supplier. The response carries the eSIM with its activation data. Pricing is always computed server-side — see eSIM pricing.

Request

Send Content-Type: application/json (the media type is read in any case).

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

response = requests.post(
    "https://api.fx-port.com/api/v1/esims/orders",
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    json={
        "package_id": "france-in-7days-1gb",
        "first_name": "John",
        "last_name": "Smith",
        "email": "[email protected]",
        "phone": "+15555550123",
        "description": "Order for John Smith",
    },
    timeout=100,
)
body = response.json()
response.raise_for_status()
booking_id = body["booking_id"]
iccid = body["data"]["iccid"]  # the same value as data.sim.iccid

Body fields

FieldTypeRequiredDescription
package_idstringYesid from the package catalog. 1 to 128 characters: letters, digits, -, _, .
emailstringYesThe traveler's email address (at most 254 characters). FX-Port asks the supplier to email the QR code and a sharing link to this address
phonestringYesThe traveler's phone number: at least 3 digits, at most 32 characters. No further format is checked. Kept on the booking; it is not sent to the supplier
first_name / last_namestringNoTraveler name, at most 100 characters. Any characters are accepted (José, Çelik-Ünal, non-Latin scripts). Kept on the booking only
descriptionstringNoA note for your own records, at most 500 characters. It is sent to the supplier with the order and returned as order.description
agent_idstring (UUID)NoThe id of the user of your agency who places the order. It is stored on the booking (agentId in the account history). It must be a user of your agency: another id is refused with invalid_agent_id. 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_emailemailThe name the booking rows use. Accepted without a warning
customer_phonephoneSame
customer_first_namefirst_nameSame
customer_last_namelast_nameSame

There is no quantity field. Every order creates exactly one eSIM — pricing and the agency balance debit only ever account for a single eSIM. quantity and every price field (price, total_price, net_price, retail_price, base_price, currency) are ignored: they are listed in the warnings of the response as ignored_field. To get more eSIMs, call this endpoint once per eSIM (on live, two orders of the same package must be 60 seconds apart: see Duplicates, timeouts and retries).

Only pass the traveler's own email and phone number — never billing, pricing, or account information. The email is sent to the supplier so that the traveler receives the QR code; the other contact fields stay in FX-Port.

Response example

A sandbox order (long values shortened with ...). The response has status 200, not 201.

{
  "environment": "sandbox",
  "success": true,
  "data": {
    "order": {
      "code": "20261101-106852",
      "order_id": "20261101-106852",
      "package_id": "france-in-7days-1gb",
      "type": "sim",
      "description": "Order for John Smith",
      "esim_type": "local",
      "created_at": "2026-11-01 15:23:22",
      "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,
      "voice": null,
      "text": null,
      "coverage": ""
    },
    "sim": {
      "iccid": "8900000540463077957",
      "created_at": "2026-11-01 15:23:22",
      "lpa": "lpa.airalo.com",
      "imsis": null,
      "qrcode": "LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741",
      "qrcode_url": "https://sandbox.airalo.com/qr?expires=1879860202&id=473564&signature=...",
      "direct_apple_installation_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741",
      "apn_type": "automatic",
      "apn_value": "N/A",
      "apn": {
        "ios": { "apn_type": "automatic", "apn_value": "N/A" },
        "android": { "apn_type": "automatic", "apn_value": "N/A" }
      },
      "is_roaming": true,
      "confirmation_code": null,
      "msisdn": null,
      "sharing": { "access_code": 8803, "link": "https://esims.cloud/v8hy7-usgkq6wg" }
    },
    "iccid": "8900000540463077957"
  },
  "booking_id": "FX-1793546602916",
  "message": "eSIM order placed successfully",
  "balanceDebited": false,
  "transactionCreated": false,
  "payoutCreated": false,
  "balance_debited": false,
  "transaction_created": false,
  "payout_created": false
}

Save data.iccid (the same value as data.sim.iccid) — it's required for topups, usage, and installation instructions. Save booking_id for booking lookups. If sim is null, the supplier has not returned the eSIM yet: the booking exists, data.iccid is the placeholder PENDING- followed by the order code, and the data of the eSIM is available through setup instructions once the supplier has it.

Response field reference

data.order

FieldTypeMeaning
codestringThe supplier's order code. order_id carries the same value: it is the name the booking rows use, and the one to use in new code
package_idstringThe package that was ordered
typestringAlways sim
descriptionstring | nullThe description you sent
esim_typestringKind of eSIM plan, for example local
created_atstringWhen the supplier created the order: YYYY-MM-DD HH:MM:SS, UTC, without a zone
packagestringName of the package: operator, data allowance and validity
datastringData allowance as text
validityintegerValidity in days
price / total_price / retail_pricenumberWhat the traveler pays, in your agency currency. All three are the retail price (retail_price is the explicit name of total_price) — in the catalog price is a different amount, see eSIM pricing
currency / agency_currencystringYour agency currency (both fields have the same value)
supplier_currency / exchange_ratestring / numberThe supplier's currency and the rate to your currency
voice / textinteger | nullVoice minutes / SMS included, or null
coveragestringCoverage text taken from the installation instructions; can be an empty string

data.sim — the activation data is sensitive: show it only to the traveler who owns the eSIM

FieldTypeMeaning
iccidstringThe ICCID of the eSIM
lpastring | nullHost of the SM-DP+ server
qrcodestring | nullThe activation string the QR code encodes: LPA:1$<host>$<activation code>
qrcode_urlstring | nullLink to the QR code image
direct_apple_installation_urlstring | nullLink that starts the installation on an iPhone
apn_type / apn_value / apnstring / string / objectThe access point name, per platform in apn
is_roamingboolean | nullWhether the eSIM works through roaming
imsis, confirmation_code, msisdnstring | nullExtra identifiers, null for most plans
sharingobject | nullaccess_code and link: a page where the traveler can open the eSIM
created_atstring | nullWhen the supplier created the eSIM, same format as the order

Top level

FieldMeaning
booking_idThe FX-Port booking reference, FX- followed by a number. It is top-level, not inside data
data.iccidThe ICCID of the eSIM, the same value as data.sim.iccid; PENDING- followed by the order code while sim is null
messageA confirmation sentence
balanceDebited, transactionCreated, payoutCreatedSandbox only, always false: nothing was debited, no balance transaction and no supplier payout were created. balance_debited, transaction_created and payout_created carry the same values and are the names to use in new code. Also present on the error bodies of this endpoint

What happens in the sandbox

A sandbox key creates a test eSIM at the supplier's sandbox: the activation code is a dummy (DUMMY-...), the qrcode_url is on the supplier's sandbox host, no balance is checked or debited, and the three flags above are false. The booking is stored with environment: "sandbox" and appears in bookings and in the account history for sandbox keys only. The balance check, the 60-second duplicate check and the balance lock run on live only.

Duplicates, timeouts and retries

There is no idempotency key. FX-Port protects you from a double order in two ways:

  • while an order for the same package is being processed, a second order for that package is refused with 409 duplicate_request (the check is on the package, whoever the traveler is);
  • on live, an order for a package is refused with the same code when the most recent confirmed order of your agency is for that same package and is less than 60 seconds old, and the response names it in existing_booking_id. The traveler does not matter: two eSIMs of the same package for two travelers must be ordered at least 60 seconds apart on live (the sandbox has no such wait).

Orders usually answer in a few seconds, but a slow supplier can take much longer, and a connection closes after about 100 seconds. If a call times out or the connection drops, do not send the order again at once: the eSIM may exist. Look for it first with GET /esims/bookings?search=<traveler email> (the list is newest first), and only order again when no booking was created.

If the supplier refuses the order, the response is a supplier_error with the supplier's HTTP status, nothing is debited and a booking with the status failed is saved for support.

Errors

HTTPcodeCause
400missing_fieldpackage_id, email or phone is missing
400invalid_package_id, invalid_email, invalid_phone, invalid_name, invalid_description, invalid_quantity, 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)
404not_foundNo package has this id, or it has no price. The message names the package_id
409duplicate_requestAn order for this package is being processed, or (live) the latest confirmed order of your agency is for this package and is less than 60 seconds old
429rate_limit_exceededOver 1,200 requests per minute for this key
429concurrency_limit_exceededToo many orders of your key or agency are being processed at the same time. Nothing was ordered: retry after the seconds in Retry-After
supplier statussupplier_errorThe supplier refused the order. The status is the supplier's (for example 422 or 429), nothing was debited, and the body has the booking_id of the failed booking
500booking_record_failed, balance_debit_failed, supplier_response_invalidThe supplier created the eSIM but FX-Port could not save the booking or take the debit, or the supplier's answer was unusable. Do not retry: contact support with the order_code or 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 ordered. Retry in a few seconds (Retry-After tells how long when it is set)

The 403 for a read key is returned before the body is read, so it appears even for an empty body. When a booking is saved for a failed order, its booking_id is in the error body: use it when you contact support.

PreviousPackage catalogNext Top up an eSIM

On this page

  • Request
  • Body fields
  • Response example
  • Response field reference
  • What happens in the sandbox
  • Duplicates, timeouts and retries
  • Errors

© 2026 FX-Port

TermsPrivacy[email protected]