NewREST API with an OpenAPI 3.1 file
eSIM API for travel agencies, OTAs and resellers
Sell travel eSIMs through one REST API. List data packages by country with prices in your agency currency, order one for a traveler, give the traveler the QR code, follow usage and top up. One base URL serves sandbox and live. Begin with a free sandbox key; live keys work after your agency is approved.
- Base URL
https://api.fx-port.com/api/v1- Authentication
Authorization: Bearer fxp_test_...- Key prefixes
fxp_test_ / fxp_live_- Endpoints
- 12
- Countries with packages
- 190+
- Code sample languages
- 9
The eSIM API, call by call
Follow an eSIM sale step by step as each request is built, sent and answered. The sessions cover selling an eSIM, keeping a traveler connected, checking devices and bookings, and the failures worth handling. The requests and answers are examples from the documentation, and this page never calls the API.
3 requests: List the packages, Order the eSIM, Read the setup data.
Illustrative session: the bodies are examples from the documentation, dates are moved forward so they stay ahead of today, and times are illustrative. This page sends no request.
Download the Postman collection
Find a package, order it, read the setup data
Start with the packages of one country, order one for a traveler, then read the setup data of the order. The sandbox creates a test eSIM with a dummy activation code and debits nothing, and the console shows a trimmed copy of the catalog answer.
List the packages
The catalog call takes
countryandtypeand returns the matching packages, with prices in your agency currency. Keep thepackage_idof the one the traveler picks.Request GET /esims/packages curl 'https://api.fx-port.com/api/v1/esims/packages?country=france&type=local' \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 180 ms · 3.6 KB 200 OK Content-Type: application/json Cache-Control: private, no-store X-Environment: sandbox ratelimit-limit: 6000 ratelimit-policy: 6000;w=60 { "environment": "sandbox", "success": true, "endpoint": "/packages", "data": [ { "id": "france-in-7days-1gb", "package_id": "france-in-7days-1gb", "type": "sim", "price": 402.5, "base_price": 402.5, "amount": 1024, "day": 7, "is_unlimited": false, "title": "1 GB - 7 days", "is_fair_usage_policy": false, "fair_usage_policy": null, "data": "1 GB", "voice": null, "text": null, "country_name": "France", "country_code": "FR", "country_slug": "france", // ... "operator_name": "KPN France Data only", "operator_id": 1704, // ... "operator_type": "local", "operator_is_prepaid": false, "operator_esim_type": "Prepaid", "operator_warning": null, "operator_apn_type": "automatic", "operator_is_roaming": true, // ... "operator_plan_type": "data", "operator_activation_policy": "first-usage", "operator_is_kyc_verify": false, "operator_rechargeability": true, // ... // ... 1 more // ... "is_regional": false, // ... 10 more }, // ... ], "meta": { "total_packages": 12, "filters_applied": { // ... 2 more }, "locale": "en", "agency_currency": "DZD", "currency": "DZD", "supplier_currency": "USD", "exchange_rate": 250, "commission_percentage": "12 %", "commission_rate": 12 } }Example trimmed for display
- PASS Status code is 200
- PASS $.success equals true
- PASS $.data[0].package_id exists
- PASS $.data[0].balance_debit_amount exists
Order the eSIM
Creating an order takes the
package_idwith the traveler's email address and phone number. Keepbooking_idanddata.iccidfrom the answer. In the sandbox,balanceDebited,transactionCreatedandpayoutCreatedare allfalse.Request POST /esims/orders curl https://api.fx-port.com/api/v1/esims/orders \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb", "first_name": "John", "last_name": "Smith", "email": "[email protected]", "phone": "+15555550123", "description": "Order for John Smith" }'Response 200 OK · 900 ms · 1.5 KB 200 OK Content-Type: application/json Cache-Control: private, no-store X-Environment: sandbox ratelimit-limit: 1200 ratelimit-policy: 1200;w=60 x-fx-limit-class: booking { "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-09 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, "voice": null, "text": null, "coverage": "" }, "sim": { "iccid": "8900000540463077957", "created_at": "2026-11-09 15:23:22", "lpa": "lpa.airalo.com", "imsis": null, "qrcode": "LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741", // ... "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": { // ... 2 more }, "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, "balance_debited": false, "transactionCreated": false, "transaction_created": false, "payoutCreated": false, "payout_created": false }Example trimmed for display
- PASS Status code is 200
- PASS $.environment equals sandbox
- PASS $.balance_debited equals false
- PASS $.data.iccid exists
Read the setup data
The setup data call takes the
booking_idand returns the QR code, the activation data, the install links and the installation steps of the order. Show them only to the traveler who owns the eSIM.Request GET /esims/bookings/{booking_id}/setup-instructions curl https://api.fx-port.com/api/v1/esims/bookings/FX-1793546602916/setup-instructions \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 210 ms · 1.8 KB 200 OK Content-Type: application/json Cache-Control: private, no-store X-Environment: sandbox ratelimit-limit: 6000 ratelimit-policy: 6000;w=60 { "environment": "sandbox", "success": true, "data": { "code": "20261101-106852", "order_id": "20261101-106852", "currency": "DZD", "package_id": "france-in-7days-1gb", "quantity": 1, "type": "sim", "description": "Order for John Smith", "esim_type": "local", "validity": 7, "package": "KPN France Data only-1 GB - 7 days", "data": "1 GB", "price": 468.84, "created_at": "2026-11-09 15:23:22", // ... "installation_guides": { "en": "https://www.airalo.com/help/getting-started-with-airalo" }, "text": null, "voice": null, "sims": [ { "created_at": "2026-11-09 15:23:22", "iccid": "8900000540463077957", "lpa": "lpa.airalo.com", "imsis": null, "qrcode": "LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741", // ... "apn_type": "automatic", "apn_value": "N/A", "is_roaming": true, "confirmation_code": null, "apn": { "ios": { "apn_type": "automatic", "apn_value": "N/A" }, "android": { "apn_type": "automatic", "apn_value": "N/A" } }, "msisdn": null, "direct_apple_installation_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741", "direct_android_installation_url": "https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741", "sharing": { "access_code": 8803, "link": "https://esims.cloud/v8hy7-usgkq6wg" } } ], "total_price": 468.84, "retail_price": 468.84, "agency_currency": "DZD" } }Example trimmed for display
- PASS Status code is 200
- PASS $.data.package_id equals france-in-7days-1gb
- PASS $.data.sims[0].iccid exists
Reference of GET /esims/bookings/{booking_id}/setup-instructions in the documentation
Read the usage, then top up
Once a traveler has an eSIM, read how much data is left, ask which packages can be added to that exact eSIM, then create the top-up. Usage is cached, so a figure can be minutes old. The top-up history is in the sidebar and in the transcript.
Read the usage
The usage call takes the ICCID of the order and returns
remainingandtotalin megabytes, thestatusand the expiry. The answer comes from a cache, andforce=trueasks the supplier again.Request GET /esims/usage/{iccid} curl https://api.fx-port.com/api/v1/esims/usage/8900000540463077957 \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 160 ms · 261 B 200 OK { "environment": "sandbox", "success": true, "data": { "data": { "remaining": 1024, "total": 1024, "expired_at": "2026-11-16 15:23:29", "is_unlimited": false, "status": "ACTIVE", "remaining_voice": 0, "remaining_text": 0, "total_voice": 0, "total_text": 0 }, "meta": { "message": "success" } } }- PASS Status code is 200
- PASS $.data.data.status equals ACTIVE
- PASS $.data.data.remaining exists
List top-up packages
This call lists the packages that can be added to this exact eSIM today. An empty list means the eSIM cannot be topped up now. Take the
package_idof the top-up from this list, never from the original order.Request GET /esims/topup-packages/{iccid} curl https://api.fx-port.com/api/v1/esims/topup-packages/8900000540463077957 \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 240 ms · 1.1 KB 200 OK { "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, // ... 12 more }, { // ... 22 more } ] } }Example trimmed for display
- PASS Status code is 200
- PASS $.data.data has 2 items
- PASS $.data.data[0].type equals topup
Reference of GET /esims/topup-packages/{iccid} in the documentation
Create the top-up
Creating a top-up takes the
package_idfrom that list and the ICCID. On live, your balance is debited only after the supplier accepts it. In the sandbox thebalancein the answer is zero and nothing is debited.Request POST /esims/topups curl https://api.fx-port.com/api/v1/esims/topups \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb-topup", "iccid": "8900000540463077957", "first_name": "John", "last_name": "Smith", "email": "[email protected]", "phone": "+15555550123" }'Response 200 OK · 900 ms · 682 B 200 OK { "environment": "sandbox", "success": true, "data": { "topup": { "code": "20261101-106853", "order_id": "20261101-106853", "package_id": "france-in-7days-1gb-topup", // ... 10 more }, "iccid": "8900000540463077957", "balance": { // ... 3 more } }, "booking_id": "FX-1793546625391", "message": "eSIM topup completed successfully", "balanceDebited": false, "balance_debited": false, "transactionCreated": false, "transaction_created": false, "payoutCreated": false, "payout_created": false }- PASS Status code is 200
- PASS $.environment equals sandbox
- PASS $.balance_debited equals false
- PASS $.data.balance.current equals 0
Read the top-up history
The top-up history lists the top-ups of one eSIM, newest first. Top-ups are not in the booking list, so this is where you find them.
Request GET /esims/bookings/{iccid}/topup-history curl https://api.fx-port.com/api/v1/esims/bookings/8900000540463077957/topup-history \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 130 ms · 615 B 200 OK { "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": "[email protected]", "customer_first_name": "John", "customer_last_name": "Smith", "customer_phone": "+15555550123", "provider_name": "Airalo", // ... 4 more } ] }- PASS Status code is 200
- PASS $.data[0].type equals topup
Reference of GET /esims/bookings/{iccid}/topup-history in the documentation
Check a device, then read your bookings
Before the sale, check that the traveler's phone supports eSIM. After the sale, read your bookings and check that a booking reference is yours. Bulk usage is listed in the sidebar.
Check the device list
The compatible devices call returns the devices known to support eSIM, with
brand,modelandos. Look the traveler's phone up before you sell.Request GET /esims/compatible-devices curl https://api.fx-port.com/api/v1/esims/compatible-devices \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 120 ms · 283 B 200 OK { "environment": "sandbox", "success": true, "data": [ { "model": "T803D", "os": "android", "brand": "Alcatel", "name": "V3 Ultra" }, { "model": "iPhone15,2", "os": "ios", "brand": "Apple", "name": "iPhone 14 Pro" } ], "meta": { "total_devices": 631, "cached_at": "2026-11-09T15:07:43.957236", "cache_age_days": 0.01 } }- PASS Status code is 200
- PASS $.success equals true
- PASS $.data[0].model exists
Reference of GET /esims/compatible-devices in the documentation
List your bookings
The booking list returns the eSIM orders of your agency, newest first. This request asks for one booking with
limit, andpaginationsays how many orders match and whether another page follows.Request GET /esims/bookings curl 'https://api.fx-port.com/api/v1/esims/bookings?limit=1' \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 150 ms · 714 B 200 OK { "environment": "sandbox", "success": true, "data": [ { "id": "639417b4-b103-4ece-bd5d-850315322278", "booking_id": "FX-1793546602916", "order_id": "20261101-106852", "status": "confirmed", "currency": "DZD", // ... 4 more "iccid": "8900000540463077957", // ... 9 more } ], "pagination": { "total": 61, "page": 1, "limit": 1, "pages": 61, "total_pages": 61, "has_more": true } }- PASS Status code is 200
- PASS $.data has 1 item
- PASS $.pagination.has_more equals true
Check ownership
The ownership check answers
is_ownertrue when the booking belongs to your agency. A booking of another agency and one that does not exist get the same answer, so the call cannot be used to discover references.Request GET /esims/bookings/{booking_id}/verify curl https://api.fx-port.com/api/v1/esims/bookings/FX-1793546602916/verify \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 90 ms · 167 B 200 OK { "environment": "sandbox", "isOwner": true, "is_owner": true, "userAgencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d", "bookingAgencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d" }- PASS Status code is 200
- PASS $.is_owner equals true
Reference of GET /esims/bookings/{booking_id}/verify in the documentation
Read usage in bulk
One call reads the usage of several eSIMs listed in
iccidsand returns one entry per ICCID, each with its ownstatus. Always sendiccids.Request GET /esims/usage/bulk curl 'https://api.fx-port.com/api/v1/esims/usage/bulk?iccids=8900000540463077957' \ -H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK · 220 ms · 321 B 200 OK { "environment": "sandbox", "success": true, "data": [ { "iccid": "8900000540463077957", "data": { "data": { "remaining": 1024, "total": 1024, "expired_at": "2026-11-16 15:23:29", "is_unlimited": false, "status": "ACTIVE", "remaining_voice": 0, // ... 3 more }, "meta": { "message": "success" } }, "status": "success" } ] }- PASS Status code is 200
- PASS $.data[0].status equals success
Failures and how to react to each
All failures share one body, and the first entry of errors has a stable code. These order calls fail on purpose, each for a different reason and with a different reaction.
Read key on an order
A Read key on the order call is refused with
403andread_write_permission_required, before the body is read. Switch to a Read+Write key.Request POST /esims/orders curl https://api.fx-port.com/api/v1/esims/orders \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb", "first_name": "John", "last_name": "Smith", "email": "[email protected]", "phone": "+15555550123", "description": "Order for John Smith" }'Response 403 Forbidden · 90 ms · 456 B 403 Forbidden { "environment": "sandbox", "success": false, "errors": [ { "code": "read_write_permission_required", "type": "permission_error", "title": "Read write permission required", "message": "This endpoint requires an API key with read_write permission.", "field": null, "details": {}, "documentation_url": "https://fx-port.com/docs/errors#read_write_permission_required" } ], "meta": { "request_id": "req_7f0c2e9a41b84d6e8a35c1d2", "status": 403 }, "request_id": "req_7f0c2e9a41b84d6e8a35c1d2" }- PASS Status code is 403
- PASS Error code is read_write_permission_required
- PASS $.errors[0].type equals permission_error
Invalid email address
A value that is not an email address is refused with
400andinvalid_email, andfieldnamesemail. Fix the value before you send the order again: the same request fails the same way.Request POST /esims/orders curl https://api.fx-port.com/api/v1/esims/orders \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb", "first_name": "John", "last_name": "Smith", "email": "traveler-at-example.com", "phone": "+15555550123", "description": "Order for John Smith" }'Response 400 Bad Request · 100 ms · 381 B 400 Bad Request { "environment": "sandbox", "success": false, "errors": [ { "code": "invalid_email", "type": "validation_error", "title": "Invalid email", "message": "email is not a valid email address", "field": "email", "details": {}, "documentation_url": "https://fx-port.com/docs/errors#invalid_email" } ], "meta": { "request_id": "req_7f0c2e9a41b84d6e8a35c1d2", "status": 400 }, "request_id": "req_7f0c2e9a41b84d6e8a35c1d2" }- PASS Status code is 400
- PASS Error code is invalid_email
- PASS $.errors[0].field equals email
Duplicate order
An order for a package that is already being processed is refused with
409andduplicate_request. Do not send it again at once: look the booking up first, and order again only if none was created.Request POST /esims/orders curl https://api.fx-port.com/api/v1/esims/orders \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb", "first_name": "John", "last_name": "Smith", "email": "[email protected]", "phone": "+15555550123", "description": "Order for John Smith" }'Response 409 Conflict · 110 ms · 423 B 409 Conflict { "environment": "sandbox", "success": false, "errors": [ { "code": "duplicate_request", "type": "conflict", "title": "Duplicate request", "message": "An identical order is already being processed. Please wait before retrying.", "field": null, "details": {}, "documentation_url": "https://fx-port.com/docs/errors#duplicate_request" } ], "meta": { "request_id": "req_7f0c2e9a41b84d6e8a35c1d2", "status": 409 }, "request_id": "req_7f0c2e9a41b84d6e8a35c1d2" }- PASS Status code is 409
- PASS Error code is duplicate_request
- PASS $.errors[0].type equals conflict
Too many requests
Over the per-minute limit, the call is refused with
429andrate_limit_exceeded, and theRetry-Afterheader says how many seconds to wait. Then retry with backoff.Request POST /esims/orders curl https://api.fx-port.com/api/v1/esims/orders \ -H "Authorization: Bearer fxp_test_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "package_id": "france-in-7days-1gb", "first_name": "John", "last_name": "Smith", "email": "[email protected]", "phone": "+15555550123", "description": "Order for John Smith" }'Response 429 Too Many Requests · 80 ms · 504 B 429 Too Many Requests Retry-After: 12 { "environment": "sandbox", "success": false, "errors": [ { "code": "rate_limit_exceeded", "type": "rate_limit_error", "title": "Rate limit exceeded", "message": "Too many requests hit the API too quickly. Please retry your request after the time specified in the `ratelimit-reset` header.", "field": null, "details": { "retry_after": 12 }, "documentation_url": "https://fx-port.com/docs/errors#rate_limit_exceeded" } ], "meta": { "request_id": "req_7f0c2e9a41b84d6e8a35c1d2", "status": 429 }, "request_id": "req_7f0c2e9a41b84d6e8a35c1d2" }- PASS Status code is 429
- PASS Error code is rate_limit_exceeded
- PASS Header Retry-After is present
- PASS $.errors[0].details.retry_after equals 12
From catalog to connected traveler in five steps
An integration is a short path: read the catalog, create the order, give the traveler the eSIM, follow the usage and top up when the data runs out. Only the order and the top-up change anything. Every other call only reads.
Read the catalog for one destination
List the packages of one country, with prices already in your agency currency, and keep the package_id of the one the traveler picks. Always send country: the whole catalog is a very large answer without pagination. Read package ids from the answer instead of hard-coding them, because the catalog changes.
Request
curl 'https://api.fx-port.com/api/v1/esims/packages?country=france&type=local' \
-H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK
{
"environment": "sandbox",
"success": true,
"endpoint": "/packages",
"data": [
{
"id": "france-in-7days-1gb",
"package_id": "france-in-7days-1gb",
"type": "sim",
"price": 402.5,
"base_price": 402.5,
// ... 28 more
"total_price": 468.84,
"balance_debit_amount": 418.6,
"b2b_price": 418.6,
"agency_commission": "12 %",
"commission_rate": 12,
"currency": "DZD",
// ... 2 more
},
// ...
],
"meta": {
"total_packages": 12,
"filters_applied": {
// ... 2 more
},
"locale": "en",
"agency_currency": "DZD",
"currency": "DZD",
"supplier_currency": "USD",
"exchange_rate": 250,
"commission_percentage": "12 %",
"commission_rate": 12
}
}Create the order
Send the package_id with the traveler's email address and phone number. One order creates exactly one eSIM, and on live it debits your agency balance. Keep the booking_id and the ICCID from the answer, because every later call needs one of them.
Request
curl https://api.fx-port.com/api/v1/esims/orders \
-H "Authorization: Bearer fxp_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"package_id": "france-in-7days-1gb",
"first_name": "John",
"last_name": "Smith",
"email": "[email protected]",
"phone": "+15555550123",
"description": "Order for John Smith"
}'Response 200 OK
{
"environment": "sandbox",
"success": true,
"data": {
"order": {
"code": "20261101-106852",
"order_id": "20261101-106852",
"package_id": "france-in-7days-1gb",
// ... 17 more
},
"sim": {
// ... 4 more
"qrcode": "LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741",
// ... 7 more
"sharing": {
"access_code": 8803,
"link": "https://esims.cloud/v8hy7-usgkq6wg"
}
},
"iccid": "8900000540463077957"
},
"booking_id": "FX-1793546602916",
"message": "eSIM order placed successfully",
"balanceDebited": false,
"balance_debited": false,
"transactionCreated": false,
"transaction_created": false,
"payoutCreated": false,
"payout_created": false
}| Field | What it holds |
|---|---|
booking_id | The FX-Port reference of the order. Keep it for the setup data, the ownership check and your own records. |
data.iccid | The identifier of the eSIM. Usage, top-ups and installation instructions are all asked by ICCID. |
data.sim.qrcode data.sim.qrcode_url | The activation string that the QR code encodes, and a link to the QR code image. |
data.sim.direct_apple_installation_url data.sim.sharing | A link that starts the installation on an iPhone, and a sharing page where the traveler can open the eSIM with its access code. The setup data call adds the link for Android. |
PENDING-, and the data is available from the setup data call once the supplier has it.Give the traveler the eSIM
The answer carries the QR code and its activation string, so you can show them in your own screens. FX-Port also asks the supplier to email the QR code and a sharing link to the traveler's address, so you do not have to relay it. The setup data call returns the same data again later, with the installation steps.
Request
curl https://api.fx-port.com/api/v1/esims/bookings/FX-1793546602916/setup-instructions \
-H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK
{
"environment": "sandbox",
"success": true,
"data": {
"code": "20261101-106852",
"order_id": "20261101-106852",
"currency": "DZD",
"package_id": "france-in-7days-1gb",
"quantity": 1,
"type": "sim",
"description": "Order for John Smith",
"esim_type": "local",
"validity": 7,
"package": "KPN France Data only-1 GB - 7 days",
"data": "1 GB",
// ... 5 more
"sims": [
{
"created_at": "2026-11-09 15:23:22",
"iccid": "8900000540463077957",
// ... 2 more
"qrcode": "LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741",
// ... 6 more
"direct_apple_installation_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$lpa.airalo.com$DUMMY-2610301358-iKFxr-44741",
// ... 2 more
}
],
// ... 3 more
}
}Follow the usage
Read the data left and the expiry of an eSIM by its ICCID. The answer is cached for up to 15 minutes, so use it for a screen or a reminder, not as a meter.
Request
curl https://api.fx-port.com/api/v1/esims/usage/8900000540463077957 \
-H "Authorization: Bearer fxp_test_YOUR_KEY"Response 200 OK
{
"environment": "sandbox",
"success": true,
"data": {
"data": {
"remaining": 1024,
"total": 1024,
"expired_at": "2026-11-16 15:23:29",
"is_unlimited": false,
"status": "ACTIVE",
"remaining_voice": 0,
"remaining_text": 0,
"total_voice": 0,
"total_text": 0
},
"meta": {
"message": "success"
}
}
}Top up when the data runs out
List the packages that can be added to this exact eSIM, then create the top-up with one of them. If the list is empty, the eSIM cannot be topped up now and the traveler needs a new one.
Request
curl https://api.fx-port.com/api/v1/esims/topups \
-H "Authorization: Bearer fxp_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"package_id": "france-in-7days-1gb-topup",
"iccid": "8900000540463077957",
"first_name": "John",
"last_name": "Smith",
"email": "[email protected]",
"phone": "+15555550123"
}'Response 200 OK
{
"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,
// ... 6 more
},
"iccid": "8900000540463077957",
"balance": {
"previous": 0,
"current": 0,
"currency": "DZD"
}
},
"booking_id": "FX-1793546625391",
"message": "eSIM topup completed successfully",
"balanceDebited": false,
"balance_debited": false,
"transactionCreated": false,
"transaction_created": false,
"payoutCreated": false,
"payout_created": false
}Prices, commission and your balance
Every package carries its money fields, computed on the server in your agency currency. A price in a request body is ignored, so an order is always created at the price the server computes.
| Field | What it holds |
|---|---|
base_price price | The package price before your commission. It is not the amount debited from your balance. |
total_price retail_price | The price to charge the traveler, with your commission included. |
balance_debit_amount b2b_price | What leaves your agency balance when you order the package on live. Read it, do not compute it. |
agency_commission commission_rate | Your commission rate, as text and as a number. Bookings return your commission as an amount instead. |
In the catalog example, the traveler pays 468.84 DZD. An order on live debits your balance 418.60 DZD, and the difference, 50.24 DZD, is your commission.
The rate belongs to your agency
The commission rate comes back with every package, and it is not the same for every agency. A contracted corporate agency can have a rate of zero, and then the total price equals the debit.
Read the fields, do not rebuild them
The debit can be higher than the base price, because amounts specific to your agency are part of it. So read balance_debit_amount and never compute it from base_price. The word price is the base price in the catalog but the retail price in orders, top-up packages and setup data: use base_price and total_price everywhere and the ambiguity disappears.
Priced in your agency currency
Every amount is already converted into your agency currency, whichever currency the supplier uses. The answer also shows the supplier currency and the exchange rate, so you can verify the rate yourself and never need to convert again.
After the sale: usage, top-ups and setup data
A sale does not end with the order. These calls keep the traveler connected, and they work only on eSIMs that your agency ordered in the same environment as your key.
Reading usage, and how fresh it is
Usage returns the data left, the allowance, the status and the expiry of one eSIM, or of up to 50 eSIMs in one call. FX-Port keeps the last answer for up to 15 minutes to protect the supplier's rate limits, so a figure can be that old. Pass force=true to ask the supplier again, for example right after a top-up. Do not do it on every request, because the supplier limits how often usage can be read. If the supplier cannot answer, you get the last known figure marked as stale.
Top-ups: ask first, then create
A top-up is two calls. First list the packages that can be added to this exact eSIM, then create the top-up with one of the returned package ids. Never reuse the package id of the original order, and do not keep a top-up package for later: ask again when the traveler wants to top up. A top-up adds data to the eSIM and does not replace what is left, so read the result from usage with force=true.
When a top-up is not possible
Suppliers choose which mobile operators serve a country, and in rare cases they change one. The eSIM can then keep working until it expires, while its package can no longer be topped up. The list of top-up packages comes back empty. If a supplier rejects a top-up, your balance is not touched and the attempt is saved as a failed booking. The answer is a supplier_error that you can react to.
Setup data and installation steps
The setup data call returns the QR code, the activation data, the install links and the installation steps of an order whenever you need them. The instructions call returns the steps per operating system, in the language you ask for. The steps are generic text from the supplier, so show them next to the QR code of the order. Show both only to the traveler who owns the eSIM.
Bookings, top-up history and ownership
The booking list returns the eSIM orders of your agency in the environment of your key, newest first. You can search it by booking id, traveler name, email address, package name or ICCID. Top-ups are not in that list: read them with the top-up history of an eSIM. Before you build a link or a screen around a booking id, the ownership check tells you whether it is yours. A booking of another agency and one that does not exist get the same answer.
Every eSIM endpoint, with OpenAPI, Postman and code samples
The eSIM API has 12 endpoints under one base URL. Every row opens the reference page of that call, with its request fields, responses, errors and examples in 9 languages.
| Endpoint | What it does | Key |
|---|---|---|
GET /api/v1/esims/packages | List the packages of one country or region, with prices already in your agency currency. Always send a country: the whole catalog is very large and has no pagination. | Read |
GET /api/v1/esims/compatible-devices | The devices known to support eSIM, for a check before the traveler buys. | Read |
POST /api/v1/esims/orders | Create one eSIM from a package for a traveler and provision it with the supplier. On live it debits your agency balance. | Read+Write |
GET /api/v1/esims/topup-packages/{iccid} | The packages that can be added to one exact eSIM today. An empty list means it cannot be topped up now. | Read |
POST /api/v1/esims/topups | Add a top-up package to an existing eSIM. On live, your balance is debited only after the supplier accepts it. | Read+Write |
GET /api/v1/esims/usage/{iccid} | The data left, the allowance, the status and the expiry of one eSIM. The answer can be up to 15 minutes old. | Read |
GET /api/v1/esims/usage/bulk | The usage of up to 50 eSIMs in one call, one entry per ICCID, each with its own status. | Read |
GET /api/v1/esims/bookings | The eSIM orders of your agency, newest first, with search and pagination. Top-ups are not in this list. | Read |
GET /api/v1/esims/bookings/{iccid}/topup-history | The top-ups of one eSIM, newest first. The list is empty when the eSIM has none. | Read |
GET /api/v1/esims/bookings/{booking_id}/verify | Check that a booking reference belongs to your agency, before you build a link or a screen around it. | Read |
GET /api/v1/esims/bookings/{booking_id}/setup-instructions | The QR code, the activation data, the install links and the installation steps of an order, as JSON. | Read |
GET /api/v1/esims/instructions/{iccid} | Step-by-step installation instructions for one ICCID, per operating system, in the language you ask for. | Read |
OpenAPI 3.1
One file describes every endpoint and every field. Generate a client from it, import it into an API tool or give it to a coding assistant.
Postman collection
The eSIM collection covers all 12 endpoints in folders that follow the stages of a sale. It holds no keys, so add your own.
Code samples in 9 languages
Every reference page prints its request in cURL, Node.js, Python, PHP, Go, Java, C#, Swift and Kotlin, generated from the OpenAPI description. Each eSIM request is available in all of them.
Docs for AI assistants
llms.txt and llms-full.txt hand AI assistants the documentation as plain text, and every docs page can be read as Markdown by adding .md to its address.
Built for production: keys, errors, retries and caching
An eSIM integration is judged in production on authentication, failures, retries, caching and rate limits. This is what to know about each.
One base URL, two kinds of key
Every request carries a key as a bearer token in the Authorization header. The prefix says where the key works: fxp_test_ for the sandbox and fxp_live_ for live. Both hit the same base URL, and each JSON answer states the environment it came from in environment. Keys belong on your server, never in a browser or an app. FX-Port keeps only a hash of each key, so a lost key cannot be shown again. A change to a key can take a few minutes to apply.
Which key does which call
A Read key can list packages, check devices, read usage and read bookings. Creating an order or a top-up needs a Read+Write key, and a Read key that attempts it gets read_write_permission_required. Any key can be locked to a list of IP addresses, and a call from another address gets ip_not_allowed. Use a Read key wherever the code only displays results.
One error format for every failure
A failed call returns an errors array. Its first entry has a stable code, a type that says how to react and the field at fault. Test the code, never the message. The error list shared by flights, hotels, eSIMs and the account holds 180 codes. When the supplier refuses an order or a top-up, the answer is a supplier_error carrying the supplier's own HTTP status. Nothing is debited and a failed booking is saved. Right after a restart, the catalog and the device list can answer 202 with success set to false: wait a few seconds and ask again.
Retrying without ordering twice
FX-Port has no idempotency key, so there is no Idempotency-Key header to send. Reads are safe to repeat; creating an order or a top-up is not. FX-Port refuses an obvious duplicate with 409 and duplicate_request: an order for a package that is still being processed. On live it also refuses the same package ordered again within 60 seconds of your agency's latest confirmed order, whoever the traveler is. Orders usually answer in a few seconds, but a slow supplier can take much longer. If a call times out or the connection drops, do not send the order again at once, because the eSIM may exist. Look for it with the search parameter of the booking list, using the traveler's email address, and order again only if nothing was created.
No webhooks: ask when you need it
FX-Port does not call a URL of yours, in the sandbox or on live. An order answers with the eSIM, or with a PENDING- placeholder while the supplier has not returned it yet. You ask for anything else when you need it: the setup data, the usage or the booking list.
Caching and speed
Usage is kept for up to 15 minutes and the list of top-up packages for a few minutes, and force=true skips the cache. The unfiltered catalog is a very large answer without pagination, so send country and accept compression with Accept-Encoding: gzip. Package titles come back in the language of the locale parameter; the first request in a language other than English is slower, because the text is fetched then and cached afterwards.
Rate limits
Limits are counted per key and per minute in two groups, and every response carries ratelimit-limit and ratelimit-remaining, so you can slow down before you are refused. After a 429, wait the seconds in Retry-After and retry with exponential backoff and jitter. Too many orders or top-ups of one key or agency running at the same time can be answered with concurrency_limit_exceeded.
| Kind of call | Per minute |
|---|---|
| Creating orders and top-ups | 1,200 |
| Every other eSIM endpoint | 6,000 |
For now the ceiling on orders and top-ups running at the same time is in observation mode: it is counted but not enforced, so nothing is refused because of it yet, and the changelog announces the day it is enforced. Read these numbers as the current policy rather than a promise. Sandbox keys have the same per-minute counters as live keys, so schedule load tests instead of running them in a loop.
Names and versions
Request fields are snake_case, such as first_name and agent_id. Earlier spellings still work: user_id is read as agent_id and adds a warnings entry, while customer_email is accepted as email without one. Many answers keep an older key beside its snake_case twin with the same value, such as order_id beside code, and ?naming=snake returns snake_case keys only. The version sits in the path, /api/v1: a breaking change would get a new prefix, while new fields and spellings are additive.
Start in the free sandbox, then go live
Everything on this page works in the sandbox today. A sandbox key is self-serve and free, and it never touches your real balance.
Create an account
Sign up for an FX-Port account. Sandbox keys are free, and sandbox bookings carry no real charges.
Create a sandbox key
On the API page of your dashboard, create a key that starts with
fxp_test_. A Read key can search and read; a Read+Write key can also create test bookings.Send your first request
There is one base URL, and the key goes in the
Authorizationheader as a bearer token. The quickstart walks through a first call.Go live when you are ready
Swap in a
fxp_live_key. Live works once your agency is approved; until then the API answers403 agency_not_active. Real bookings are paid from a funded balance.
A first catalog request in cURL. The docs print the same request in every supported language.
Request
curl 'https://api.fx-port.com/api/v1/esims/packages?country=france&type=local' \
-H "Authorization: Bearer fxp_test_YOUR_KEY"Quickstart: first sandbox call
What the sandbox does
An order in the sandbox creates a real test eSIM at the supplier, with a dummy activation code, so it cannot be installed on a phone. Nothing is debited, and the answers say so: balanceDebited, transactionCreated and payoutCreated are all false. A top-up answers a balance of zero. Usage is static: a new eSIM shows its full allowance and the status ACTIVE. Sandbox bookings are visible only to sandbox keys.
What only live can test
The balance check, the duplicate check and the balance lock run on live only. So the answers 402 with insufficient_balance, 409 for the same package ordered again within 60 seconds, and 503 with balance_lock_timeout never appear in the sandbox. Write their handling from the list of error codes.
Going live
Live keys are tied to agency approval: until your agency is approved, a live key answers agency_not_active and your sandbox key keeps working. Live orders are paid from a funded agency balance. Going live changes the key, not the code, because sandbox and live share one base URL.
Who builds on the eSIM API
Each audience leans on a different part of the same calls.
Agencies that sell trips
Agencies sell an eSIM next to the flight and the hotel. They order with the traveler's own email address, so the supplier sends the QR code and a sharing link, and they read usage when a traveler asks. The commission rate comes back with every package, and agent_id records which user of your agency placed an order.
OTAs and marketplaces
Marketplaces build the browsing page first: one country at a time, local or regional packages, the networks and the data allowance of each package, and a device check before the traveler buys. A screen that lists a traveler's eSIMs can read the usage of up to 50 of them in one call.
Travel-tech and corporate teams
Travel-tech teams and corporate travel departments build their own booking tools. Everything they need to start is public: the OpenAPI file, the Postman collection and plain-text docs that an AI coding assistant can read, so the first prototype starts from the real contract.
eSIM API: frequently asked questions
What is the FX-Port eSIM API, and who is it for?
It is a REST API that lets a travel agency, an OTA or a reseller sell travel data eSIMs from one integration. You list the packages of a destination with prices in your agency currency and order one for a traveler. You give the traveler the QR code, follow usage and top up. Everything is JSON over HTTPS, and an OpenAPI file describes every endpoint.
Is there a free sandbox, and how do I get a key?
Yes. Create an FX-Port account, open the API page of your dashboard and create a sandbox key; you can do it yourself, at no cost. An order in the sandbox creates a test eSIM with a dummy activation code, so it cannot be installed on a phone. Nothing is debited from your balance. Sandbox and live share one base URL, so the code you test is the code you ship.
What does going live require?
Your agency must be approved before a live key works. Until then a live key is refused and your sandbox key keeps working. Live orders are paid from a funded agency balance, which the sandbox never touches, and any key can be locked to a list of IP addresses. Switching means changing the key only; every answer names the environment it came from.
Which countries and packages can I sell?
Packages are available for 190+ countries, and regional bundles appear under their region. Packages differ in data allowance, validity, networks and whether they can be topped up. Read these fields and the package id from the catalog answer instead of hard-coding them. Ask for one country at a time, because the whole catalog is a very large answer. A separate device list lets you check a traveler's phone before the sale.
How does the traveler receive the eSIM?
The order answer carries the QR code, its activation string, an install link for iPhone and a sharing page. You can show them in your own screens. FX-Port also asks the supplier to email the QR code and a sharing link to the traveler's address as soon as the eSIM is created. If the supplier has not returned the eSIM yet, the order still exists and the data is available later from the setup data call.
How do prices, commission and my balance fit together?
Every package carries these money fields in your agency currency: the base price before your commission, the total price to charge the traveler, the amount debited from your balance and your commission rate. Your commission is the total price minus the debit. Read these fields instead of rebuilding them, because the debit is specific to your agency. In the sandbox nothing is debited; on live the debit leaves your agency balance.
Can I top up an eSIM, and how do I see what is left?
Yes, in two calls: list the packages that can be added to the exact eSIM, then create the top-up with one of them. A top-up is not always possible, because a supplier can change the operator of a country. An empty list then means the traveler needs a new eSIM. Usage is cached for up to 15 minutes, so treat it as a recent figure, and one call reads up to 50 eSIMs.
How are failures reported, and are there webhooks?
A failure comes back in one format with a stable error code, and that code is what your program should test, not the message text. Reads are safe to repeat with backoff. Creating an order or a top-up is different: there is no idempotency key, so after a timeout look up the booking before you try again. FX-Port refuses an obvious duplicate, such as the same package ordered twice within 60 seconds on live. There are no webhooks.
Can I cancel or refund an eSIM order through the API?
No. The API has no endpoint to cancel or refund an eSIM order. Every order creates exactly one eSIM, so check the package and the traveler's details before you send it. If the supplier refuses an order, nothing is debited and a failed booking is saved. For a problem with a specific order, write to the developer team and quote its booking id.
What do I need to start, and which tools help?
You need a sandbox key and an HTTP client. The docs add an OpenAPI 3.1 file, a Postman collection, code samples in 9 languages and plain-text exports for AI assistants. Keep your keys on your server, never in a browser or an app. The part that takes longer is your own checkout, the delivery of the QR code to the traveler and error handling.
Other FX-Port APIs
An eSIM usually goes with a trip. Flights and hotels use the same keys, the same error body and the same conventions as eSIMs, and their bookings sit in the same account history.
Search, price and book flights, pay held orders, cancel, and read tickets.
Find hotels, check rates, pre-book and book, read vouchers and cancel.
- Transfers APIPreview
Quote, book, follow and cancel airport transfers.
Rather not write code? Agencies can also sell eSIMs from the FX-Port dashboard. Sell eSIMs from the dashboard
Run your first eSIM order in the sandbox
Create an account, make a sandbox key, list packages, create an order and read usage today. Live keys follow once your agency is approved.