{
  "openapi": "3.1.0",
  "info": {
    "title": "Wanderly Catalog API",
    "version": "2.0.0",
    "summary": "Public, unauthenticated eSIM plan catalog feeds.",
    "description": "Read-only feeds of every active Wanderly eSIM data plan, for affiliates, price-comparison sites and AI agents. No API key or signup. Each endpoint returns the complete catalog in a single response. Refreshed daily; please poll no more than once per hour. Responses carry ETag and Last-Modified - send If-None-Match to get a cheap 304 and skip re-downloading an unchanged feed.",
    "contact": {
      "name": "Wanderly Support",
      "url": "https://getwanderly.com/contact"
    },
    "license": {
      "name": "Free to use with attribution",
      "url": "https://getwanderly.com/terms"
    }
  },
  "servers": [
    { "url": "https://getwanderly.com/api", "description": "Production" }
  ],
  "externalDocs": {
    "description": "Human-readable API documentation",
    "url": "https://getwanderly.com/docs/api"
  },
  "tags": [
    { "name": "catalog", "description": "Full plan catalog feeds" }
  ],
  "paths": {
    "/catalog.json": {
      "get": {
        "operationId": "getCatalogJson",
        "tags": ["catalog"],
        "summary": "Full plan catalog (v1, native schema, JSON)",
        "description": "Returns every active plan in Wanderly's native schema under `products`. There are no query filters - filter client-side on `country_code`, `coverage_type` or `plan_kind`.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The complete catalog.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CatalogV1" }
              }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" }
        }
      }
    },
    "/catalog.csv": {
      "get": {
        "operationId": "getCatalogCsv",
        "tags": ["catalog"],
        "summary": "Full plan catalog (v1, native schema, CSV)",
        "description": "The same plans as `/catalog.json`, flattened to CSV with a header row. One row per plan; columns match the properties of the Product schema.",
        "parameters": [
          { "$ref": "#/components/parameters/IfNoneMatch" },
          { "$ref": "#/components/parameters/IfModifiedSince" }
        ],
        "responses": {
          "200": {
            "description": "The complete catalog as CSV.",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Last-Modified": { "$ref": "#/components/headers/LastModified" }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" }
              }
            }
          },
          "304": { "$ref": "#/components/responses/NotModified" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "Previously returned ETag. Yields 304 when the feed is unchanged.",
        "schema": { "type": "string" }
      },
      "IfModifiedSince": {
        "name": "If-Modified-Since",
        "in": "header",
        "required": false,
        "description": "Previously returned Last-Modified value. Yields 304 when the feed is unchanged.",
        "schema": { "type": "string" }
      }
    },
    "headers": {
      "ETag": {
        "description": "Opaque version tag for the current feed contents.",
        "schema": { "type": "string" }
      },
      "LastModified": {
        "description": "Timestamp of the most recent catalog refresh.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "NotModified": {
        "description": "The feed has not changed since the supplied ETag / Last-Modified. No body is returned."
      }
    },
    "schemas": {
      "CatalogV1": {
        "type": "object",
        "required": ["provider", "currency", "generated_at", "count", "products"],
        "properties": {
          "provider": { "type": "string", "const": "Wanderly" },
          "website": { "type": "string", "format": "uri" },
          "currency": { "type": "string", "description": "ISO 4217 code for all prices in this feed.", "example": "USD" },
          "generated_at": { "type": "string", "format": "date-time", "description": "When this feed was built. Use for change detection." },
          "count": { "type": "integer", "description": "Number of plans in `products`.", "example": 2231 },
          "products": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Product" }
          }
        }
      },
      "Product": {
        "type": "object",
        "description": "A single purchasable eSIM data plan.",
        "required": ["product_id", "sku", "name", "country_code", "currency", "price"],
        "properties": {
          "product_id": { "type": "string", "description": "Stable unique plan identifier. Use as your primary key.", "example": "PC2G2GAZG" },
          "sku": { "type": "string", "description": "Stable supplier SKU.", "example": "AF_1_7" },
          "name": { "type": "string", "example": "Afghanistan 1GB 7Days" },
          "country_code": { "type": "string", "description": "ISO 3166-1 alpha-2 code of the primary destination.", "example": "AF" },
          "country_name": { "type": "string", "example": "Afghanistan" },
          "coverage": { "type": "string", "description": "Comma-separated ISO codes covered by the plan.", "example": "AF" },
          "coverage_type": { "type": "string", "enum": ["Single", "Regional", "Global"] },
          "plan_kind": { "type": "string", "enum": ["total", "daily"], "description": "`total` = fixed bucket of data for the whole validity; `daily` = allowance per day." },
          "data_type": { "type": "string", "example": "Data in Total" },
          "data_gb": { "type": "number", "description": "Included data in GB. Per day when `plan_kind` is `daily`.", "example": 1 },
          "validity_days": { "type": "integer", "description": "Validity in days from activation.", "example": 7 },
          "unlimited": { "type": "boolean", "description": "True for unlimited plans. Fair-usage limits may still apply." },
          "network_speed": { "type": "string", "description": "Maximum network generation.", "example": "4G/5G" },
          "hotspot": { "type": "boolean", "description": "Whether tethering / hotspot is permitted." },
          "kyc_required": { "type": "boolean", "description": "Whether the destination requires ID verification." },
          "topup_supported": { "type": "boolean" },
          "topup_type": { "type": ["string", "null"] },
          "currency": { "type": "string", "example": "USD" },
          "price": { "type": "number", "description": "Live retail price, tax exclusive.", "example": 8.99 },
          "price_per_gb": { "type": "number", "description": "Derived price-efficiency metric, useful for ranking.", "example": 8.99 },
          "url": { "type": "string", "format": "uri", "description": "Destination page for this plan's country." },
          "buy_url": { "type": "string", "format": "uri", "description": "Direct deep link that preselects this plan." },
          "last_updated": { "type": "string", "format": "date-time", "description": "Last time this plan's data or price changed." }
        }
      }
    }
  }
}
