{
  "openapi": "3.1.0",
  "info": {
    "title": "Geo-Parity API",
    "version": "1.0.0",
    "description": "Free, key-less JSON APIs behind geo-parity.com: purchasing power parity, fair global salaries, Schengen 90/180 compliance, cross-timezone meetings, contractor economics, freelance rates, tax-residency day counts, country comparison, cloud region latency, and a digital-nomad visa directory. All GET responses are cacheable; errors return { ok:false, code, message, example }. No key is required: anonymous callers get 300 requests/hour per IP. An optional API key (Authorization: Bearer gp_live_..., created at /account) only raises that ceiling — responses are identical either way. Every response carries X-RateLimit-Limit / -Remaining / -Reset; exceeding the limit returns 429 with Retry-After. Figures are informational estimates, not financial/tax/legal advice. Machine discovery: /llms.txt and /.well-known/mcp.json.",
    "contact": { "url": "https://geo-parity.com" }
  },
  "servers": [{ "url": "https://geo-parity.com" }],
  "security": [{}, { "bearerAuth": [] }, { "apiKeyQuery": [] }],
  "paths": {
    "/api/calculate.json": {
      "get": {
        "operationId": "calculatePpp",
        "summary": "PPP calculation for a US-baseline income in a target country",
        "parameters": [
          { "name": "income", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0 } },
          { "name": "target", "in": "query", "required": true, "schema": { "type": "string" }, "description": "ISO alpha-2 country code" },
          { "name": "source", "in": "query", "schema": { "type": "string", "enum": ["consensus", "worldbank", "imf", "oecd", "pwt", "worldbank-cons", "bigmac", "factbook"] } },
          { "name": "city", "in": "query", "schema": { "type": "string" }, "description": "Optional city slug for a sub-national Capital-to-Rural adjustment (e.g. london)" }
        ],
        "responses": { "200": { "description": "Metrics: effectivePurchasingPower, lifestyleEquilibriumCost, purchasingPowerRatio (city-adjusted when city given)" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/wage-bands.json": {
      "get": {
        "operationId": "calculateWageBands",
        "summary": "Local wage distribution: survival, minimum, median, top 10% and top 1% bands",
        "parameters": [
          { "name": "country", "in": "query", "required": true, "schema": { "type": "string" }, "description": "ISO country code" },
          { "name": "salary", "in": "query", "schema": { "type": "number" }, "description": "Locate your pay on the curve (>20000 read as annual)" },
          { "name": "percentile", "in": "query", "schema": { "type": "number", "minimum": 0, "maximum": 100 }, "description": "Exact threshold wage at this percentile" }
        ],
        "responses": { "200": { "description": "Bands (monthly + annual, USD + local), model params, optional standing/threshold" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/salary.json": {
      "get": {
        "operationId": "localizeSalary",
        "summary": "Cost-of-living-fair salary localization (single target or full matrix)",
        "parameters": [
          { "name": "base", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0 } },
          { "name": "home", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "target", "in": "query", "schema": { "type": "string" }, "description": "Omit for a matrix across every country" },
          { "name": "source", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "results[]: ratio, localEquivalent, local currency at market FX" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/visa-check": {
      "post": {
        "operationId": "checkSchengen",
        "summary": "Schengen 90/180 compliance check (+ optional planned-entry projection)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["trips"],
                "properties": {
                  "trips": { "type": "array", "items": { "type": "object", "required": ["entry", "exit"], "properties": { "entry": { "type": "string", "format": "date" }, "exit": { "type": "string", "format": "date" } } } },
                  "referenceDate": { "type": "string", "format": "date" },
                  "plannedEntry": { "type": "string", "format": "date", "description": "Adds plan: { maxStay, latestExit, fullResetDate }" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "compliant, daysUsed, daysRemaining, firstOverstayDate, maxDaysInWindow" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/meeting.json": {
      "get": {
        "operationId": "findMeetingSlot",
        "summary": "Team timezone friction score + working-hours overlap (DST-accurate)",
        "parameters": [
          { "name": "zones", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Comma-separated IANA timezones" },
          { "name": "start", "in": "query", "schema": { "type": "number", "default": 540 } },
          { "name": "end", "in": "query", "schema": { "type": "number", "default": 1080 } },
          { "name": "hours", "in": "query", "schema": { "type": "string" }, "description": "Per-zone overrides: '540-1080;420-720'" },
          { "name": "date", "in": "query", "schema": { "type": "string", "format": "date" } }
        ],
        "responses": { "200": { "description": "overlap window (UTC + per-participant local) or bestPartial detail, plus a friction block (score 0–100, grade, dailyOverlapHours, coverage, loneWolf)" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/contract.json": {
      "get": {
        "operationId": "compareContract",
        "summary": "Contractor vs employee net take-home and break-even rate",
        "parameters": [
          { "name": "contractorRate", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "employeeRate", "in": "query", "required": true, "schema": { "type": "number" } },
          { "name": "country", "in": "query", "schema": { "type": "string", "default": "US" } },
          { "name": "taxMode", "in": "query", "schema": { "type": "string", "enum": ["flat", "brackets"] }, "description": "brackets = progressive 2025 schedules (US/GB only)" },
          { "name": "hours", "in": "query", "schema": { "type": "number" } },
          { "name": "contractorWeeks", "in": "query", "schema": { "type": "number" } },
          { "name": "employeeWeeks", "in": "query", "schema": { "type": "number" } },
          { "name": "expenses", "in": "query", "schema": { "type": "number" } },
          { "name": "health", "in": "query", "schema": { "type": "number" } },
          { "name": "ctRate", "in": "query", "schema": { "type": "number" }, "description": "percent" },
          { "name": "empRate", "in": "query", "schema": { "type": "number" }, "description": "percent" }
        ],
        "responses": { "200": { "description": "contractor/employee nets, breakevenRate, verdict" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/rate.json": {
      "get": {
        "operationId": "requiredRate",
        "summary": "Freelance hourly/day rate required for a desired net income",
        "parameters": [
          { "name": "desiredNet", "in": "query", "required": true, "schema": { "type": "number", "minimum": 1 } },
          { "name": "country", "in": "query", "schema": { "type": "string", "default": "US" } },
          { "name": "hours", "in": "query", "schema": { "type": "number", "default": 40 } },
          { "name": "weeksOff", "in": "query", "schema": { "type": "number", "default": 4 } },
          { "name": "utilization", "in": "query", "schema": { "type": "number", "default": 0.75 } },
          { "name": "expenses", "in": "query", "schema": { "type": "number", "default": 0 } },
          { "name": "health", "in": "query", "schema": { "type": "number" } },
          { "name": "taxRate", "in": "query", "schema": { "type": "number" }, "description": "percent" }
        ],
        "responses": { "200": { "description": "hourlyRate, dayRate, requiredGross, billableHours" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/residency-check": {
      "post": {
        "operationId": "checkResidency",
        "summary": "Tax-residency day counts (183-day, US SPT, UK tax year)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["stays"],
                "properties": {
                  "stays": { "type": "array", "items": { "type": "object", "required": ["country", "entry", "exit"], "properties": { "country": { "type": "string" }, "entry": { "type": "string", "format": "date" }, "exit": { "type": "string", "format": "date" } } } },
                  "referenceDate": { "type": "string", "format": "date" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "per-country daysByYear + rule evaluations" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/residency-tracker": {
      "post": {
        "operationId": "trackResidency",
        "summary": "Unified Schengen 90/180 + per-country tax residency + tax cost, from one itinerary",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["stays"],
                "properties": {
                  "stays": { "type": "array", "items": { "type": "object", "required": ["country", "entry", "exit"], "properties": { "country": { "type": "string" }, "entry": { "type": "string", "format": "date" }, "exit": { "type": "string", "format": "date" } } } },
                  "income": { "type": "number", "description": "optional gross annual USD — enables tax-cost projection" },
                  "referenceDate": { "type": "string", "format": "date" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "schengen block, per-country risk (threshold/basis/safeDaysLeft/risk, Cyprus-60 secondary, US SPT, UK, taxProjection) and a summary" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/compare.json": {
      "get": {
        "operationId": "compareCountries",
        "summary": "Two-country cost-of-living comparison across all datasets",
        "parameters": [
          { "name": "a", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "b", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "spreads, ratioAtoB, pctDifference, salary100k both directions" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/region.json": {
      "get": {
        "operationId": "selectRegion",
        "summary": "Cloud region ranking by population-weighted latency",
        "parameters": [
          { "name": "users", "in": "query", "required": true, "schema": { "type": "string" }, "description": "region:percent pairs, comma-separated" },
          { "name": "hosts", "in": "query", "schema": { "type": "string" } },
          { "name": "provider", "in": "query", "schema": { "type": "string", "enum": ["AWS", "GCP", "Azure"] } }
        ],
        "responses": { "200": { "description": "ranking, best, optimal split, best two-region pair" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/fire.json": {
      "get": {
        "operationId": "findFireLocations",
        "summary": "Geo-arbitrage FIRE analysis: where a portfolio's passive income funds each lifestyle tier",
        "parameters": [
          { "name": "portfolio", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0 }, "description": "Total portfolio (USD)" },
          { "name": "swr", "in": "query", "schema": { "type": "number", "default": 0.04 }, "description": "Safe withdrawal rate, fraction (0.04) or percent (4)" },
          { "name": "source", "in": "query", "schema": { "type": "string", "enum": ["consensus", "worldbank", "imf", "oecd", "pwt", "worldbank-cons"] } },
          { "name": "savings", "in": "query", "schema": { "type": "number" }, "description": "Monthly savings (USD) — enables yearsToTier" },
          { "name": "return", "in": "query", "schema": { "type": "number", "default": 0.05 }, "description": "Expected real annual return, fraction or percent" },
          { "name": "coast", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Coast-FIRE mode (no further savings)" }
        ],
        "responses": { "200": { "description": "Per-country tier costs, FIRE numbers, achieved tier, visa eligibility, local income, SWR sensitivity" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/micro-costs.json": {
      "get": {
        "operationId": "estimateMicroCosts",
        "summary": "Everyday price estimates for a 32-item basket in two countries (Pint & Property Index)",
        "parameters": [
          { "name": "base", "in": "query", "required": true, "schema": { "type": "string" }, "description": "ISO country code to compare from" },
          { "name": "target", "in": "query", "required": true, "schema": { "type": "string" }, "description": "ISO country code to compare to" },
          { "name": "source", "in": "query", "schema": { "type": "string", "enum": ["consensus", "worldbank", "imf", "oecd", "pwt", "worldbank-cons", "bigmac", "factbook"] }, "description": "Dataset override; auto-picks the consumption basket when it covers both countries" },
          { "name": "baseCity", "in": "query", "schema": { "type": "string" }, "description": "Optional base-side city slug (sub-national adjustment, e.g. london)" },
          { "name": "targetCity", "in": "query", "schema": { "type": "string" }, "description": "Optional target-side city slug (e.g. hull) — enables intra-country city comparisons" }
        ],
        "responses": { "200": { "description": "Per-item USD + local prices with pctDiff, category subtotals, monthly budget rollup (city-adjusted when cities given)" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/tax-trap.json": {
      "get": {
        "operationId": "checkTaxResidencyRisk",
        "summary": "Tax-residency risk radar: distance to the threshold and cost of crossing (standard vs expat regime)",
        "parameters": [
          { "name": "country", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "days", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0, "maximum": 366 }, "description": "Days of presence" },
          { "name": "income", "in": "query", "required": true, "schema": { "type": "number", "minimum": 0 }, "description": "Annual income (USD)" },
          { "name": "arrival", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Adds leaveBy/residencyDate" }
        ],
        "responses": { "200": { "description": "risk band, days remaining, standard-vs-regime tax projection, leave-by dates, Schengen cross-warning" }, "400": { "$ref": "#/components/responses/MissingParams" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/livability.json": {
      "get": {
        "operationId": "rankLivability",
        "summary": "Rank countries by weighted quality-of-life metrics vs monthly essentials cost",
        "parameters": [
          { "name": "budget", "in": "query", "schema": { "type": "number", "default": 3000 }, "description": "Monthly budget (USD) for lifestyle labels + visa eligibility" },
          { "name": "minInternet", "in": "query", "schema": { "type": "number" }, "description": "Minimum median broadband, Mbps" },
          { "name": "minSafety", "in": "query", "schema": { "type": "number", "minimum": 0, "maximum": 100 } },
          { "name": "maxBudget", "in": "query", "schema": { "type": "number" }, "description": "Essentials-cost ceiling (USD)" },
          { "name": "weights", "in": "query", "schema": { "type": "string" }, "description": "internet,safety,healthcare,air,english — five 0–10 values, default 5,5,5,5,5" }
        ],
        "responses": { "200": { "description": "Ranked rows: composite + value scores, essentials cost, lifestyle label, visa + tax badges" }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/nomad-visas.json": {
      "get": {
        "operationId": "findNomadVisas",
        "summary": "Match income + household to digital-nomad visas",
        "parameters": [
          { "name": "income", "in": "query", "schema": { "type": "number" }, "description": "your gross monthly income (in currency, default USD)" },
          { "name": "currency", "in": "query", "schema": { "type": "string" }, "description": "ISO code for income, e.g. EUR (default USD)" },
          { "name": "partners", "in": "query", "schema": { "type": "number", "enum": [0, 1] }, "description": "1 if bringing a partner" },
          { "name": "children", "in": "query", "schema": { "type": "number" }, "description": "number of dependent children" },
          { "name": "minDuration", "in": "query", "schema": { "type": "number" }, "description": "months" },
          { "name": "renewable", "in": "query", "schema": { "type": "string", "enum": ["1"] } },
          { "name": "residency", "in": "query", "schema": { "type": "string", "enum": ["1"] } }
        ],
        "responses": { "200": { "description": "matches[] you qualify for (with household requiredUsd) + nearMisses[]; official source URLs" }, "400": { "description": "Unsupported currency." }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/passport.json": {
      "get": {
        "operationId": "passportPower",
        "summary": "Passport visa-free access score, global rank and destination breakdown",
        "parameters": [
          { "name": "country", "in": "query", "schema": { "type": "string" }, "description": "Passport alpha-2 code (e.g. US, SG). Omit for the ranked league table." }
        ],
        "responses": { "200": { "description": "mobilityScore, rank, percentile and destinations grouped by access type (or the league table)" }, "404": { "description": "No passport data for the given country." }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/runway.json": {
      "get": {
        "operationId": "calculateRunway",
        "summary": "Extra months of savings runway from relocating (fixed/local burn split)",
        "parameters": [
          { "name": "savings", "in": "query", "required": true, "schema": { "type": "number" }, "description": "total savings (in currency)" },
          { "name": "burn", "in": "query", "required": true, "schema": { "type": "number" }, "description": "total monthly burn (in currency)" },
          { "name": "fixed", "in": "query", "schema": { "type": "number" }, "description": "location-independent slice of burn" },
          { "name": "currency", "in": "query", "schema": { "type": "string" }, "description": "ISO code for money inputs (default USD)" },
          { "name": "home", "in": "query", "schema": { "type": "string" }, "description": "home country code (default US)" },
          { "name": "targets", "in": "query", "required": true, "schema": { "type": "string" }, "description": "1–3 hubs: COUNTRY or COUNTRY:citySlug, comma-separated" }
        ],
        "responses": { "200": { "description": "home baseline + hubs[] with runwayMonths, extraMonths, multiplier, runsOut" }, "400": { "description": "Unknown currency, home or target." }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List your API keys (browser session required; prefixes only, never the key)",
        "responses": { "200": { "description": "{ ok: true, keys: [{ id, prefix, label, createdAt, lastUsedAt }] }" }, "401": { "description": "Not signed in." } }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create an API key (browser session required; the raw key is returned exactly once)",
        "responses": { "201": { "description": "{ ok: true, id, key, prefix } — store `key` now; it is never shown again." }, "401": { "description": "Not signed in." }, "422": { "description": "TOO_MANY_KEYS — revoke one first (max 5)." } }
      },
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Revoke an API key by id (browser session required)",
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "{ ok: true }" }, "401": { "description": "Not signed in." }, "404": { "description": "No such key for this account." } }
      }
    },
    "/api/airports.json": {
      "get": {
        "operationId": "airports",
        "summary": "Airport lookup by IATA, by country, or nearest to a coordinate",
        "parameters": [
          { "name": "iata", "in": "query", "schema": { "type": "string" }, "description": "3-letter IATA code, e.g. BKK" },
          { "name": "country", "in": "query", "schema": { "type": "string" }, "description": "ISO alpha-2 country code, e.g. TH" },
          { "name": "near", "in": "query", "schema": { "type": "string" }, "description": "lat,lon — returns nearest airports with great-circle distanceKm" },
          { "name": "limit", "in": "query", "schema": { "type": "number", "default": 5 }, "description": "Max results for a `near` query" }
        ],
        "responses": { "200": { "description": "Matching airports (IATA, name, city, country, lat, lon, size; distanceKm for near)" }, "400": { "description": "No query parameter supplied." }, "404": { "description": "Unknown IATA code." }, "422": { "$ref": "#/components/responses/InvalidInput" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    }
  },
  "components": {
    "responses": {
      "MissingParams": { "description": "Required parameters missing; body includes code, message, and a worked example." },
      "InvalidInput": { "description": "Well-formed but invalid input; body includes a machine-readable code (e.g. UNKNOWN_TARGET) and message." },
      "RateLimited": {
        "description": "Hourly rate limit exceeded; body is { ok:false, code:\"RATE_LIMITED\", message, retryAfterSeconds }. Honour Retry-After and retry after the reset.",
        "headers": {
          "Retry-After": { "description": "Seconds until the window resets.", "schema": { "type": "integer" } },
          "X-RateLimit-Limit": { "description": "Requests allowed per hour for this caller.", "schema": { "type": "integer" } },
          "X-RateLimit-Remaining": { "description": "Requests left in the current window.", "schema": { "type": "integer" } },
          "X-RateLimit-Reset": { "description": "Window reset, epoch seconds.", "schema": { "type": "integer" } }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "Optional Geo-Parity API key (gp_live_...), created at /account. Raises the hourly rate limit; never changes a response." },
      "apiKeyQuery": { "type": "apiKey", "in": "query", "name": "key", "description": "The same key, for clients that cannot set headers." }
    }
  }
}
