{
  "info": {
    "_postman_id": "fxport-api-esims-sandbox-v1",
    "name": "FX-Port API - eSIMs (Sandbox)",
    "description": "API-facing FX-Port eSIM collection for external/sandbox consumers.\n\nAuth is defined once at the collection root as Bearer {{api_key}}, so every request inherits the same sandbox API key. Only sandbox keys (fxp_test_...) should be used with this collection. External API keys select live or sandbox from the key itself. Requests do not pass an environment parameter.\n\nMultiple suppliers: FX-Port sources eSIM packages from multiple eSIM suppliers, not a single one. Which supplier fulfills a given package is an internal detail; coverage and rates keep improving as FX-Port adds suppliers and as each supplier expands its own network.\n\nPermissions: only 'Create eSIM Order' and 'Create eSIM Topup' require a read+write key. Every other request works with a read-only key.\n\nCommission: agencies are charged a flat 12% commission on top of the B2B price for every package order and topup (0% for corporate/contracted agencies). Kiosk and platform-internal commissions are never included in any response. Each package/topup also reports 'supplier_currency' and 'exchange_rate' (supplier_currency -> your agency currency) so conversion is transparent.\n\nRequired contact details: 'email' and 'phone' are REQUIRED on 'Create eSIM Order' and 'Create eSIM Topup' (not optional) \u2014 the supplier emails the QR code and setup instructions directly to that address as soon as the order/topup is confirmed. Only pass the traveler's/client's own contact details \u2014 never billing or pricing data.\n\nSupplier continuity: on rare occasions the fulfilling supplier may change the underlying network operator for a country, which can block a topup on an eSIM issued under the previous operator. Always call 'Topup Packages For ICCID' first \u2014 if it returns no compatible packages, the eSIM cannot be topped up and a new eSIM order should be placed instead. 'Create eSIM Topup' never debits the agency balance unless the supplier accepts the topup.\n\nChaining: this collection auto-chains requests. 'List Packages' captures 'esim_package_id'. 'Create eSIM Order' captures 'esim_iccid' and 'esim_booking_id'. 'Topup Packages For ICCID' captures 'esim_topup_package_id' from its own response. 'Create eSIM Topup' captures 'esim_topup_booking_id'. Run the folder '02 - Orders And Topups' top-to-bottom to see the full chain.\n\nIncluded API-facing endpoints:\n- eSIM package catalog\n- Compatible devices\n- Orders and topups\n- Usage, booking history, topup history, setup instructions, and installation instructions\n\nIntentionally excluded because they are internal/cache/debug only:\n- POST /api/v1/esims/packages\n- GET /api/v1/esims/packages/status\n- POST /api/v1/esims/packages/refresh\n- GET /api/v1/esims/token/status\n- POST /api/v1/esims/token/refresh\n- GET /api/v1/esims/compatible-devices/status\n- POST /api/v1/esims/compatible-devices/refresh\n- GET /api/v1/esims/bookings/{booking_id}/verify (internal ownership check, no external use case)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_key}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.fx-port.com"
    },
    {
      "key": "api_key",
      "value": "fxp_test_YOUR_KEY"
    },
    {
      "key": "webhook_secret",
      "value": "whsec_YOUR_SECRET"
    },
    {
      "key": "esim_country_slug",
      "value": "france"
    },
    {
      "key": "esim_filter_type",
      "value": "local"
    },
    {
      "key": "esim_package_id",
      "value": ""
    },
    {
      "key": "esim_topup_package_id",
      "value": ""
    },
    {
      "key": "esim_iccid",
      "value": ""
    },
    {
      "key": "esim_booking_id",
      "value": ""
    },
    {
      "key": "esim_topup_booking_id",
      "value": ""
    },
    {
      "key": "traveler_first_name",
      "value": "Ana"
    },
    {
      "key": "traveler_last_name",
      "value": "Kova"
    },
    {
      "key": "traveler_email",
      "value": "ana.kova@example.com"
    },
    {
      "key": "traveler_phone",
      "value": "+34600123456"
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "const pick = arr => arr[Math.floor(Math.random() * arr.length)];",
          "// Randomize the traveler identity on each run. Intentionally NOT Algeria-only —",
          "// FX-Port serves agencies and travelers worldwide, so examples use a diverse,",
          "// non-region-specific pool of names, countries and phone prefixes.",
          "if (/order|topup/i.test((pm.info && pm.info.requestName) || '') || !pm.collectionVariables.get('traveler_first_name')) {",
          "  const people = [",
          "    { first: 'Ana', last: 'Kova', cc: '+34', dial: () => `6${Math.floor(1e8 + Math.random() * 9e7)}` },",
          "    { first: 'Marco', last: 'Rossi', cc: '+39', dial: () => `3${Math.floor(1e8 + Math.random() * 9e7)}` },",
          "    { first: 'Elena', last: 'Petrova', cc: '+359', dial: () => `8${Math.floor(1e7 + Math.random() * 9e6)}` },",
          "    { first: 'Noah', last: 'Andersson', cc: '+46', dial: () => `70${Math.floor(1e6 + Math.random() * 9e5)}` },",
          "    { first: 'Yuki', last: 'Tanaka', cc: '+81', dial: () => `90${Math.floor(1e7 + Math.random() * 9e6)}` },",
          "    { first: 'Priya', last: 'Nair', cc: '+91', dial: () => `9${Math.floor(1e8 + Math.random() * 9e7)}` },",
          "    { first: 'Kwame', last: 'Mensah', cc: '+233', dial: () => `24${Math.floor(1e6 + Math.random() * 9e5)}` },",
          "    { first: 'Santiago', last: 'Vidal', cc: '+54', dial: () => `11${Math.floor(1e6 + Math.random() * 9e5)}` },",
          "    { first: 'Ingrid', last: 'Haugen', cc: '+47', dial: () => `4${Math.floor(1e7 + Math.random() * 9e6)}` },",
          "    { first: 'Wei', last: 'Zhang', cc: '+65', dial: () => `8${Math.floor(1e6 + Math.random() * 9e5)}` }",
          "  ];",
          "  const p = pick(people);",
          "  pm.collectionVariables.set('traveler_first_name', p.first);",
          "  pm.collectionVariables.set('traveler_last_name', p.last);",
          "  pm.collectionVariables.set('traveler_email', `${p.first.toLowerCase()}.${p.last.toLowerCase()}@example.com`);",
          "  pm.collectionVariables.set('traveler_phone', `${p.cc}${p.dial()}`);",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "pm.test('HTTP status is successful or accepted', function () {",
          "  pm.expect(pm.response.code).to.be.within(200, 299);",
          "});"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "01 - Catalog",
      "item": [
        {
          "name": "List Packages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/packages?locale=en",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "packages"],
              "query": [{ "key": "locale", "value": "en" }]
            },
            "description": "Cached package catalog with agency-specific pricing. Read-only.\n\nEach package includes 'price' (B2B price, what is debited from your balance), 'retail_price'/'total_price' (price + 12% agency commission \u2014 what you charge the traveler; 0% for corporate/contracted agencies), and 'supplier_currency'/'exchange_rate' showing the currency conversion used. No kiosk or platform-internal commission is ever included."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "try {",
                  "  const body = pm.response.json();",
                  "  const rows = Array.isArray(body.data) ? body.data : [];",
                  "  if (rows.length && rows[0].id) {",
                  "    pm.collectionVariables.set('esim_package_id', rows[0].id);",
                  "  }",
                  "} catch (error) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "List Packages By Country And Type",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/packages?country={{esim_country_slug}}&filter%5Btype%5D={{esim_filter_type}}&locale=en",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "packages"],
              "query": [
                { "key": "country", "value": "{{esim_country_slug}}" },
                { "key": "filter[type]", "value": "{{esim_filter_type}}" },
                { "key": "locale", "value": "en" }
              ]
            },
            "description": "Package catalog filtered by country slug and package type. `esim_country_slug` / `esim_filter_type` are plain FX-Port catalog filters — which upstream supplier(s) they resolve to is an internal detail."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "try {",
                  "  const body = pm.response.json();",
                  "  const rows = Array.isArray(body.data) ? body.data : [];",
                  "  if (rows.length && rows[0].id) {",
                  "    pm.collectionVariables.set('esim_package_id', rows[0].id);",
                  "  }",
                  "} catch (error) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "Compatible Devices",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/compatible-devices",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "compatible-devices"],
              "query": []
            },
            "description": "Cached list of devices known to support eSIM. Read-only. Can briefly return `success: false` with an empty list while the cache is still loading right after a deployment — retry after a few seconds."
          }
        }
      ]
    },
    {
      "name": "02 - Orders And Topups",
      "item": [
        {
          "name": "Create eSIM Order",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"package_id\": \"{{esim_package_id}}\",\n  \"first_name\": \"{{traveler_first_name}}\",\n  \"last_name\": \"{{traveler_last_name}}\",\n  \"email\": \"{{traveler_email}}\",\n  \"phone\": \"{{traveler_phone}}\",\n  \"description\": \"Sandbox eSIM order\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/esims/orders",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "orders"],
              "query": []
            },
            "description": "Create an eSIM order. Requires read_write.\n\nRequired fields: package_id, email, phone. Sending a request without email or phone returns HTTP 400 — they are not optional. The supplier emails the QR code and installation instructions directly to 'email' as soon as the order is confirmed.\n\nTraveler names must use plain Latin (ASCII) letters only \u2014 no accents/diacritics and no non-Latin scripts (transliterate to the romanized Latin form). Allowed: A-Z, a-z, spaces, hyphens.\n\nPricing (price + 12% agency commission = total_price) is computed server-side; any pricing fields in the request body are ignored.\n\nRun this request first in this folder — it captures esim_iccid and esim_booking_id for the next two requests."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const packageId = pm.collectionVariables.get('esim_package_id');",
                  "if (!packageId) {",
                  "  throw new Error('esim_package_id is required. Run 01 - Catalog / List Packages first, or set esim_package_id manually.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "try {",
                  "  const body = pm.response.json();",
                  "  const bookingId = body.booking_id;",
                  "  if (bookingId) pm.collectionVariables.set('esim_booking_id', bookingId);",
                  "  const iccid = body.data && body.data.sim && body.data.sim.iccid;",
                  "  if (iccid) pm.collectionVariables.set('esim_iccid', iccid);",
                  "} catch (error) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "Topup Packages For ICCID",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/topup-packages/{{esim_iccid}}?locale=en&force=false",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "topup-packages", "{{esim_iccid}}"],
              "query": [
                { "key": "locale", "value": "en" },
                { "key": "force", "value": "false" }
              ]
            },
            "description": "Get available topup packages for an existing eSIM ICCID. Read-only lookup.\n\nThis calls the fulfilling supplier live for the given ICCID, so the returned list always reflects current availability for that exact SIM \u2014 including the rare case where the underlying network operator serving that country has changed since the eSIM was originally issued. An empty result means this eSIM cannot currently be topped up; place a new eSIM order for that traveler instead.\n\nRun this after 'Create eSIM Order' \u2014 it captures esim_topup_package_id for 'Create eSIM Topup'."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const iccid = pm.collectionVariables.get('esim_iccid');",
                  "if (!iccid) {",
                  "  throw new Error('esim_iccid is required. Run Create eSIM Order first, or set esim_iccid manually.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "try {",
                  "  const body = pm.response.json();",
                  "  const rows = (body.data && Array.isArray(body.data.data)) ? body.data.data : [];",
                  "  if (rows.length && rows[0].id) {",
                  "    pm.collectionVariables.set('esim_topup_package_id', rows[0].id);",
                  "  } else {",
                  "    console.warn('No topup packages returned for this ICCID — see the Supplier continuity doc before retrying.');",
                  "  }",
                  "} catch (error) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "Create eSIM Topup",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"package_id\": \"{{esim_topup_package_id}}\",\n  \"iccid\": \"{{esim_iccid}}\",\n  \"first_name\": \"{{traveler_first_name}}\",\n  \"last_name\": \"{{traveler_last_name}}\",\n  \"email\": \"{{traveler_email}}\",\n  \"phone\": \"{{traveler_phone}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/esims/topups",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "topups"],
              "query": []
            },
            "description": "Top up an existing eSIM. Requires read_write.\n\nRequired fields: package_id, iccid, email, phone. Sending a request without email or phone returns HTTP 400 — they are not optional.\n\nAlways run 'Topup Packages For ICCID' first and only pass a package_id it returned \u2014 topup availability is fetched live from the supplier per ICCID, so it correctly reflects rare cases where the underlying network operator for that country has changed since the eSIM was issued. If the supplier rejects the topup (e.g. because the original operator no longer serves that ICCID), no balance is debited and no booking is recorded as confirmed.\n\nTopping up adds the new package's data/validity on top of what the eSIM already has rather than replacing it — re-check 'Single eSIM Usage' afterward to see the resulting totals for this specific package."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const iccid = pm.collectionVariables.get('esim_iccid');",
                  "if (!iccid) {",
                  "  throw new Error('esim_iccid is required. Run Create eSIM Order first, or set esim_iccid manually.');",
                  "}",
                  "const packageId = pm.collectionVariables.get('esim_topup_package_id');",
                  "if (!packageId) {",
                  "  throw new Error('esim_topup_package_id is required. Run Topup Packages For ICCID first, or set esim_topup_package_id manually.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "try {",
                  "  const body = pm.response.json();",
                  "  const bookingId = body.booking_id;",
                  "  if (bookingId) pm.collectionVariables.set('esim_topup_booking_id', bookingId);",
                  "} catch (error) {}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "03 - Usage And Bookings",
      "item": [
        {
          "name": "Single eSIM Usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/usage/{{esim_iccid}}?force=false",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "usage", "{{esim_iccid}}"],
              "query": [{ "key": "force", "value": "false" }]
            },
            "description": "Usage for one ICCID. Read-only.\n\nUsage is cached for up to 15 minutes per ICCID to protect supplier rate limits \u2014 it is not guaranteed real-time. Set force=true to bypass the cache when you need the freshest number (e.g. right after a topup), but avoid doing this on every request."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const iccid = pm.collectionVariables.get('esim_iccid');",
                  "if (!iccid) {",
                  "  throw new Error('esim_iccid is required. Run Create eSIM Order first, or set esim_iccid manually.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Bulk eSIM Usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/usage/bulk?force=false",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "usage", "bulk"],
              "query": [{ "key": "force", "value": "false" }]
            },
            "description": "Usage for all eSIMs owned by the authenticated agency. Read-only. Subject to the same 15-minute per-ICCID cache as single usage; per-entry `status` is `success`, `rate_limited`, or `error`."
          }
        },
        {
          "name": "List eSIM Bookings",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/bookings?page=1&limit=50&search=",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "bookings"],
              "query": [
                { "key": "page", "value": "1" },
                { "key": "limit", "value": "50" },
                { "key": "search", "value": "" }
              ]
            },
            "description": "Read-only eSIM booking list for the authenticated agency. Only agency-facing fields are returned — supplier source cost, FX-Port's internal platform margin, and any kiosk/partner-level commission are never included."
          }
        }
      ]
    },
    {
      "name": "04 - Instructions And History",
      "item": [
        {
          "name": "Setup Instructions By Booking",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/bookings/{{esim_booking_id}}/setup-instructions",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "bookings", "{{esim_booking_id}}", "setup-instructions"],
              "query": []
            },
            "description": "Retrieve setup data (QR code, APN, manual/QR install steps) for a booking. Read-only."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const bookingId = pm.collectionVariables.get('esim_booking_id');",
                  "if (!bookingId) {",
                  "  throw new Error('esim_booking_id is required. Run Create eSIM Order first, or set esim_booking_id manually.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Installation Instructions By ICCID",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/instructions/{{esim_iccid}}?locale=en",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "instructions", "{{esim_iccid}}"],
              "query": [{ "key": "locale", "value": "en" }]
            },
            "description": "Structured, step-by-step installation instructions per platform for one ICCID. Read-only."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const iccid = pm.collectionVariables.get('esim_iccid');",
                  "if (!iccid) {",
                  "  throw new Error('esim_iccid is required. Run Create eSIM Order first, or set esim_iccid manually.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Topup History By ICCID",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/esims/bookings/{{esim_iccid}}/topup-history",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "esims", "bookings", "{{esim_iccid}}", "topup-history"],
              "query": []
            },
            "description": "Topup history for an ICCID. Read-only."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const iccid = pm.collectionVariables.get('esim_iccid');",
                  "if (!iccid) {",
                  "  throw new Error('esim_iccid is required. Run Create eSIM Order first, or set esim_iccid manually.');",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
