eSimerge

Catalog

Discover the destinations and plans your account can sell.

GET /v1/catalog

Returns the list of plans available to your account. Supports filters and pagination.

Query parameters

  • scopecountry, regional, or global.
  • typeunlimited · fixed (data-only) · calling (voice + data). Alias: datafixed.
  • country — ISO-2 country code (filters when scope=country).
  • region — region code (filters when scope=regional).
  • search — free-text match on plan name.
  • page (default 1) + per_page (default 1000, max 1000) — page-based pagination.
  • limit / offset — equivalent offset-based pagination (still supported).
  • updated_since (alias since) — ISO-8601 timestamp or Unix seconds. Returns only plans whose updated_at is at or after that moment.

The full catalog is large (18,000+ plans). Walk every page with page=1,2,3… until meta.current_page reaches meta.last_page (or follow links.next until it is null). A full ingest at per_page=1000 is ~18 requests, well inside the 100 req/min rate limit.

# Full catalog ingest
curl 'https://portal.esimerge.com/api/public/v1/catalog?page=1&per_page=1000' -H 'Authorization: Bearer esm_sk_live_...'
# then page=2, page=3 ... until links.next is null

Copy-paste pagination loops

Each example walks links.next from page 1 until it is null and prints a final summary.

#!/usr/bin/env bash
# Full 18k-plan ingest with curl + jq. Writes catalog.json.
KEY="esm_sk_live_..."
URL="https://portal.esimerge.com/api/public/v1/catalog?page=1&per_page=1000"
echo '[]' > catalog.json
pages=0
while [ -n "$URL" ] && [ "$URL" != "null" ]; do
  body=$(curl -sS "$URL" -H "Authorization: Bearer $KEY")
  jq -s '.[0] + .[1].data' catalog.json <(echo "$body") > catalog.tmp && mv catalog.tmp catalog.json
  pages=$((pages+1))
  echo "page $(echo "$body" | jq -r '.meta.current_page')/$(echo "$body" | jq -r '.meta.last_page')"
  URL=$(echo "$body" | jq -r '.links.next')
done
echo "Imported $(jq 'length' catalog.json) plans across $pages pages"

Incremental sync — updated_since

After a first full ingest, fetch only what changed. Pass updated_since (alias since) as an ISO-8601 timestamp or Unix seconds; the API returns plans whose updated_at is at or after that moment, paginated exactly the same way. Store the timestamp you started the run at and reuse it next time — that avoids missing plans changed mid-run. Every plan object now carries updated_at, and the response echoes the normalised updated_since you sent.

# Only plans changed since 1 July 2026
curl 'https://portal.esimerge.com/api/public/v1/catalog?updated_since=2026-07-01T00:00:00Z&page=1&per_page=1000' \
  -H 'Authorization: Bearer esm_sk_live_...'

# Unix seconds work too
curl 'https://portal.esimerge.com/api/public/v1/catalog?since=1782000000' -H 'Authorization: Bearer esm_sk_live_...'
// Node: nightly delta sync
const cursor = await loadCursor();               // ISO string from the previous run
const runStartedAt = new Date().toISOString();   // save this only after a clean run
let url = `https://portal.esimerge.com/api/public/v1/catalog?updated_since=${encodeURIComponent(cursor)}&per_page=1000`;
let changed = 0;
while (url) {
  const body = await (await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } })).json();
  await upsertPlans(body.data);
  changed += body.data.length;
  url = body.links.next;
}
await saveCursor(runStartedAt);
console.log(`${changed} plans changed since ${cursor}`);
curl 'https://portal.esimerge.com/api/public/v1/catalog?scope=country&country=SA' \
  -H 'Authorization: Bearer esm_sk_test_...'

Example response

{
  "object": "list",
  "count": 1,
  "data": [
    {
      "id": "esm_plan_sa_5gb_30d",
      "object": "plan",
      "name": "Saudi Arabia 5GB / 30 days",
      "scope": "country",
      "type": "data",
      "country_code": "SA",
      "country_name": "Saudi Arabia",
      "region_code": null,
      "data_mb": 5120,
      "minutes": 0,
      "sms": 0,
      "validity_days": 30,
      "quantity": 1,
      "price_sar": 35.00,
      "price_usd": 9.33,
      "networks": ["STC", "Mobily", "Zain"],
      "coverage": [
        { "country_code": "SA", "country_name": "Saudi Arabia", "networks": ["STC", "Mobily", "Zain"] }
      ],
      "coverage_count": 1,
      "socials": {
        "tiktok": { "ios": true, "android": false },
        "facebook": { "ios": true, "android": true },
        "x": { "ios": true, "android": true },
        "snapchat": { "ios": true, "android": true },
        "google": { "ios": true, "android": true },
        "telegram": { "ios": true, "android": true },
        "chatgpt": { "ios": true, "android": true },
        "gemini": { "ios": true, "android": true },
        "claude": { "ios": true, "android": true }
      }
    }
  ],
  "total": 18432,
  "meta": {
    "current_page": 1,
    "per_page": 1000,
    "last_page": 19,
    "total": 18432,
    "from": 1,
    "to": 1000
  },
  "links": {
    "first": "https://portal.esimerge.com/api/public/v1/catalog?page=1&per_page=1000",
    "prev": null,
    "next": "https://portal.esimerge.com/api/public/v1/catalog?page=2&per_page=1000",
    "last": "https://portal.esimerge.com/api/public/v1/catalog?page=19&per_page=1000"
  },
  "limit": 1000,
  "offset": 0,
  "has_more": true,
  "next_offset": 1000
}

GET /v1/catalog/{id}

Fetch a single plan by id.

curl https://portal.esimerge.com/api/public/v1/catalog/esm_plan_sa_5gb_30d \
  -H 'Authorization: Bearer esm_sk_test_...'

Plan fields

FieldTypeDescription
idstringPlan identifier (use to issue orders). Prefix: esm_plan_
namestringDisplay name in English.
scopestringcountry | regional | global
typestringdata | data_voice | data_voice_sms
country_codestring?ISO-2 code when scope=country.
country_namestring?Country name when scope=country.
region_codestring?Region code when scope=regional (e.g. EU, GCC).
data_mbnumberData allowance in megabytes (0 = unlimited).
minutesnumberBundled voice minutes (0 if not included).
smsnumberBundled SMS count (0 if not included).
validity_daysnumberValidity from activation, in days.
quantitynumberHow many eSIMs can be issued in one order (usually 1).
price_sarnumberPartner price in USD.
price_usdnumberPartner price in USD.
networksstring[]Flat list of every mobile network reachable on this plan, across all covered countries.
coverageobject[]Countries included in the plan: [{ country_code, country_name, networks[] }]. One entry for country plans; every covered country for regional and global plans.
coverage_countnumberNumber of countries in coverage.
fair_usagestring?Free-form fair-usage policy text.
socialsobject?Destination-wide social app support, split per device platform. Keys: tiktok, facebook, x, snapchat, google, telegram, chatgpt, gemini, claude — each an object { ios: boolean, android: boolean }. null when no data is available for the destination.
supports_topupbooleantrue when the plan can be extended with top-ups (extra data / extra days). When false, the top-up endpoints return topup_not_supported.