{
  "openapi": "3.0.3",
  "info": {
    "title": "cityparity REST API",
    "version": "0.1.0",
    "description": "Cost-of-living and quality-of-life comparison endpoints. Same compute as the MCP server at /mcp; this surface is for non-MCP clients (web apps, scripts, integrations). All responses are JSON with shape { ok, data } on success or { ok: false, error, message } on failure.",
    "contact": {
      "url": "https://cityparity.com/contact/"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://mcp.cityparity.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:8788",
      "description": "Local dev (wrangler)"
    }
  ],
  "tags": [
    {
      "name": "cities",
      "description": "Discovery and per-city profiles"
    },
    {
      "name": "comparison",
      "description": "Cross-city scenario math"
    },
    {
      "name": "ranking",
      "description": "Quality-of-life ranking"
    },
    {
      "name": "safety-net",
      "description": "Family + healthcare safety-net data"
    }
  ],
  "paths": {
    "/api/v1/cities": {
      "get": {
        "tags": [
          "cities"
        ],
        "summary": "List all supported cities",
        "description": "Returns city slugs grouped by country.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional case-insensitive country filter (e.g. \"Norway\", \"United States\")."
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/cities/{slug}": {
      "get": {
        "tags": [
          "cities"
        ],
        "summary": "Get a city profile",
        "description": "Tax shape, headline costs, safety-net values, inbound regime presence, data year range.",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/cities/{slug}/inbound-regime": {
      "get": {
        "tags": [
          "cities"
        ],
        "summary": "Get inbound-worker tax regime for a city",
        "description": "Returns regime details for cities in countries with modeled regimes (Italy, Portugal, Belgium, Poland, Greece); returns regime: null otherwise.",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/compare": {
      "post": {
        "tags": [
          "comparison"
        ],
        "summary": "Compare two cities for a household scenario",
        "description": "Returns take-home pay, full cost breakdown, equivalent target salary, lifestyle deltas, quality score. RSU income is not a parameter; treated as source-only.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompareRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/rank": {
      "post": {
        "tags": [
          "ranking"
        ],
        "summary": "Rank cities by composite quality-of-life score",
        "description": "Five-dimensional composite score (financial, healthcare, vacation, childcare, safety_net) with customizable weights and filters. Same scenario applied uniformly across cities.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RankRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/safety-net": {
      "post": {
        "tags": [
          "safety-net"
        ],
        "summary": "Get safety-net values for N cities",
        "description": "Parental leave, vacation, public holidays, universal healthcare flag, plus the safety_net dimension score (0-100).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SafetyNetRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "City slug (e.g. \"nyc\", \"oslo\"). Discover slugs via GET /api/v1/cities."
      }
    },
    "responses": {
      "Ok": {
        "description": "Success",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Success"
            }
          }
        }
      },
      "Error": {
        "description": "Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Success": {
        "type": "object",
        "required": [
          "ok",
          "data"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "ok",
          "error",
          "message"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "enum": [
              "tool_error",
              "validation_error",
              "bad_json",
              "not_found",
              "rate_limited"
            ]
          },
          "message": {
            "type": "string"
          },
          "hints": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CompareRequest": {
        "type": "object",
        "required": [
          "source_city",
          "target_city",
          "gross_salary"
        ],
        "properties": {
          "source_city": {
            "type": "string",
            "description": "City slug for the current city."
          },
          "target_city": {
            "type": "string",
            "description": "City slug for the destination city."
          },
          "gross_salary": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "User's annual gross salary in the SOURCE city's local currency."
          },
          "household": {
            "type": "object",
            "properties": {
              "has_partner": {
                "type": "boolean"
              },
              "partner_gross_salary": {
                "type": "number",
                "minimum": 0,
                "description": "In source-city currency. Required when has_partner=true."
              },
              "partner_works_in_source": {
                "type": "boolean",
                "description": "Required when has_partner=true; cityparity does not silently default this."
              },
              "partner_works_in_target": {
                "type": "boolean",
                "description": "Required when has_partner=true."
              },
              "kids_ages": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 25
                }
              }
            }
          },
          "living": {
            "type": "object",
            "properties": {
              "housing_mode": {
                "type": "string",
                "enum": [
                  "rent",
                  "own"
                ]
              },
              "bedrooms": {
                "type": "integer",
                "minimum": 0,
                "maximum": 5
              },
              "lifestyle": {
                "type": "string",
                "enum": [
                  "frugal",
                  "typical",
                  "generous"
                ]
              }
            }
          },
          "advanced": {
            "type": "object",
            "properties": {
              "retirement_contrib_pct": {
                "type": "number",
                "minimum": 0,
                "maximum": 50
              },
              "age_bracket": {
                "type": "string",
                "enum": [
                  "18-24",
                  "25-34",
                  "35-44",
                  "45-54",
                  "55-64"
                ]
              },
              "trips_home_per_year": {
                "type": "integer",
                "minimum": 0,
                "maximum": 12
              },
              "source_transit_choice": {
                "type": "string",
                "enum": [
                  "car",
                  "transit"
                ]
              },
              "target_transit_choice": {
                "type": "string",
                "enum": [
                  "car",
                  "transit"
                ]
              },
              "apply_inbound_regime": {
                "type": "boolean"
              }
            }
          },
          "score_weights": {
            "$ref": "#/components/schemas/ScoreWeights"
          }
        }
      },
      "RankRequest": {
        "type": "object",
        "properties": {
          "weights": {
            "$ref": "#/components/schemas/ScoreWeights"
          },
          "filter": {
            "type": "object",
            "properties": {
              "country": {
                "type": "string"
              },
              "countries": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "region": {
                "type": "string",
                "enum": [
                  "europe",
                  "asia",
                  "north_america",
                  "south_america",
                  "oceania"
                ]
              },
              "has_universal_healthcare": {
                "type": "boolean"
              },
              "include_cities": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "exclude_cities": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "scenario": {
            "type": "object",
            "properties": {
              "gross_salary_usd": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "has_partner": {
                "type": "boolean"
              },
              "partner_gross_salary_usd": {
                "type": "number",
                "minimum": 0
              },
              "kids_ages": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 25
                }
              },
              "bedrooms": {
                "type": "integer",
                "minimum": 0,
                "maximum": 5
              },
              "housing_mode": {
                "type": "string",
                "enum": [
                  "rent",
                  "own"
                ]
              },
              "lifestyle": {
                "type": "string",
                "enum": [
                  "frugal",
                  "typical",
                  "generous"
                ]
              }
            }
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 10
          }
        }
      },
      "SafetyNetRequest": {
        "type": "object",
        "required": [
          "cities"
        ],
        "properties": {
          "cities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 20,
            "description": "Array of city slugs."
          }
        }
      },
      "ScoreWeights": {
        "type": "object",
        "description": "Override composite-score weights. Defaults: financial 30, healthcare 20, vacation 15, childcare 15, safety_net 20.",
        "properties": {
          "financial": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "healthcare": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "vacation": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "childcare": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "safety_net": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          }
        }
      }
    }
  }
}