SafarieSIM Partner Catalog API

Read-only, authenticated catalog of SafarieSIM eSIM plans for comparison and affiliate platforms (esims.io, eSIMDB, eSIMRating, …).

Base URL

https://api.safariesim.com/functions/v1/partner-catalog

Version: 2026-09-24 (returned in the X-Api-Version header and in every response body as api_version). Breaking changes ship as a new version string; additive fields may appear at any time.

Authentication

Send the partner key on every request:

x-api-key: <partner key>

Authorization: Bearer <partner key> also works. Keys are issued per partner and stored only as SHA-256 hashes. A missing, unknown, or revoked key returns 401.

Request

GET /partner-catalog

Parameter Type Default Description
page integer 1 1-based page number
per_page integer 100 Max 250
country string – ISO-3166 alpha-2. Returns every plan whose coverage includes it (local and regional), e.g. KE
destination string – Destination slug filter, e.g. kenya
currency string USD Display currency for price
format string catalog catalog, esims-io, or esimdb
days string 1,3,7,15,30 Durations to price for day-based destinations

Arbitrary durations (days)

SafarieSIM sells day-based plans from a calendar, so any whole number of days is purchasable — the default response only showcases 1, 3, 7, 15, 30. Ask for the durations you need:

?days=10            single duration
?days=1,5,10,21     explicit list
?days=1-30          inclusive range
?days=all           every day from 1 to 30

Values outside a destination's configured min_days/max_days are skipped; invalid values return 400 invalid_days. The durations actually priced are echoed back as days in the response body. Fixed-catalog destinations ignore this parameter — their plans have set validity.

Supported currencies are those with an active conversion rate (USD, EUR, GBP, PLN, AED, SAR, BRL, IDR, KES, MUR, TWD, SGD, NZD, AUD, NGN). An unsupported value returns 400 unsupported_currency.

Response

{
  "api_version": "2026-09-24",
  "generated_at": "2026-09-14T16:56:38.024Z",
  "currency": "USD",
  "pagination": { "page": 1, "per_page": 100, "total": 980, "total_pages": 10 },
  "data": [
    {
      "id": "kenya-7d",
      "name": "Kenya eSIM — 7 days",
      "destination_slug": "kenya",
      "destination_name": "Kenya",
      "country_iso2": "KE",
      "countries": ["KE"],
      "region": "Africa",
      "coverage_type": "Nationwide",
      "data_amount_mb": null,
      "is_unlimited": true,
      "validity_days": 7,
      "price": 31.25,
      "price_usd": 31.25,
      "currency": "USD",
      "operators": [{ "name": "Safaricom", "network_types": ["5G", "4G", "3G"] }],
      "network_types": ["5G", "4G", "3G"],
      "plan_type": "unlimited",
      "topup_available": false,
      "status": "available",
      "product_url": "https://safariesim.com/esim-kenya",
      "checkout_url": "https://safariesim.com/esim-kenya?days=7&checkout=1",
      "last_updated": "2026-04-04T14:02:53.642235+00:00"
    }
  ]
}

Field notes

  • id — stable. Day-based destinations use <slug>-<days>d; fixed-catalog destinations use the variant UUID.
  • price — retail price for one traveller for the whole plan, in currency, including the charm rounding customers see. price_usd is the same amount before currency conversion.
  • data_amount_mb — null when the plan is unlimited or the allowance is not published; check is_unlimited first.
  • countries — every country the plan covers (single-country plans return one entry; regional plans such as Africa Safari return the full list).
  • checkout_url — deep link that opens the product page with the plan preselected. Append your affiliate parameter (?ref=<code>) when linking.

Errors

{ "error": { "code": "unauthorized", "message": "Invalid or revoked partner API key." },
  "api_version": "2026-09-24" }
Status Code Meaning
400 unsupported_currency / unsupported_format Bad query parameter
401 unauthorized Missing, unknown, or revoked key
405 method_not_allowed Only GET is supported
500 internal_error Temporary failure; retry with backoff

Freshness and price integrity

Prices are computed at request time from the same resolver the checkout uses (supabase/functions/_shared/catalogPricing.ts), so a catalog price always matches the price a customer is charged. Responses carry Cache-Control: public, max-age=300; polling more often than every 5 minutes gives no fresher data. Please stay under the issued hourly rate limit (600 requests/hour by default).

The price source is admin-controlled at /sys/admin/daily-rates → "Partner catalog pricing": daily-rate pricing only, fixed plan prices only, or automatic. The active choice is echoed as pricing_source in every response.

Wholesale cost and supplier configuration are never exposed through any public or partner endpoint.

Partner formats

format=esims-io and format=esimdb return the same plans with each partner's preferred field names. New partners get an adapter in supabase/functions/partner-catalog/index.ts (adapt()); the normalized catalog shape stays unchanged.

eSIMDB (Provider API v1.0.2)

Keys set to the eSIMDB format (admin → partner keys → Format) need no query string:

GET /partner-catalog                     DataPlan[] (raw JSON array, full catalog)
GET /partner-catalog/destination-links   DestinationLink[]
  • Daily unlimited plans publish dataCap = daily high-speed allowance, dataCapPer: "day", and reducedSpeed (kbps) after the cap.
  • With no days parameter the eSIMDB format returns every purchasable duration (1–30 days for calendar destinations; fixed packages keep their real validity). days= still filters.
  • Destination Links only list destinations that have at least one published plan.
  • Canary Islands and Kosovo use IC and XK.
  • Each coverages[] entry lists only the networks recorded for that country (one entry per operator, types merged); a country with no recorded networks gets networks: [].
  • Prices are the final public checkout price in USD, with no referral markup.
  • Plans whose fair-use allowance is unknown are left out rather than published as dataCap: 0. The count is returned in the X-Excluded-Plans header. Fill in the missing values in the destination_fair_use settings to include them.

MCP server (Africa Travel Connectivity)

The same catalog is available to AI assistants and agents as a public Model Context Protocol server:

https://api.safariesim.com/functions/v1/mcp

It is read-only and needs no key: anyone can connect it to Claude, Cursor, VS Code or another MCP client. Full documentation, tool schemas and examples live in the public repository, punivox/africa-travel-connectivity.

Tools

Tool What it answers
search_esim_plans Plans that work in every country of a trip, filtered by trip length, data, budget and currency, cheapest first
compare_country_plans Cheapest and cheapest-unlimited plan per country, operators, and single plans covering all countries
get_country_networks Mobile operators and network types per country, and which plans use them
calculate_safari_data Data needed for a trip from its length and activities, matched to plans that fit
list_destinations Covered countries and multi-country plans (Africa first)

Plans and prices come from the same builder as GET /partner-catalog, so the MCP returns exactly the prices this API publishes for the same destination, trip length and currency. Supplier names and wholesale data are never included.

Partner keys and attribution

A partner key is optional. Sent as x-api-key (or Authorization: Bearer), it adds your ref=<code> to every link the tools return and uses your key's hourly rate limit; requests count toward the same quota as this API. A key that is unknown or revoked is rejected with 401 instead of being treated as anonymous.

claude mcp add --transport http safariesim https://api.safariesim.com/functions/v1/mcp \
  --header "x-api-key: <partner key>"

Without a key, each IP address gets a flood-protection budget (120 requests per minute).

Protocol notes

  • Serves MCP 2026-07-28 and, for older clients, the 2025-11-25 era (initialize handshake) on the same URL.
  • Stateless: no Mcp-Session-Id, single JSON responses, GET and DELETE return 405, and subscriptions/listen is refused because the tool list never changes.
  • Catalog reads are cached for up to 60 seconds; checkout always re-prices.
  • HTTP-level failures return a JSON-RPC error body with id: null:
Status JSON-RPC code error.data.code Meaning
401 -32001 unauthorized Unknown or revoked partner key
403 -32003 forbidden Key's affiliate account is not active
429 -32029 rate_limited Limit reached; wait Retry-After seconds

A bad tool argument (unknown country, unsupported currency, invalid trip length) comes back as a tool result with isError: true and a message saying how to fix the call.

Multiple suppliers

Plans come from every supplier (eSIM Access, eSIM Go, Airalo, …). Each supplier has a small mapper in supabase/functions/_shared/catalog/providers.ts that turns its private config into public facts: fair use, speed, top-up, and breakout country. Admin fair-use settings take priority over supplier data. New comparator formats go in _shared/catalog/formats.ts.

Issuing and revoking keys

Keys live in public.partner_api_keys (admin-only). Store the SHA-256 hash and a short prefix, never the raw key. Revoke by setting revoked_at.

How this document is published

This file is the single source of truth for the API contract:

  • It renders publicly at safariesim.com/api (alias /partners/api) — edit the Markdown, deploy, and the page updates.
  • The machine-readable contract lives at /partner-catalog.openapi.json (OpenAPI 3.1, importable into Postman/Swagger).

When supabase/functions/partner-catalog/index.ts changes, update both this file and the OpenAPI spec in the same change, and bump api_version for any breaking change. Plan building lives in supabase/functions/_shared/catalog/buildCatalog.ts and is shared with the MCP server (supabase/functions/mcp/index.ts); a change there affects both, so keep the "MCP server" section above and the public repository's README in sync too.