{
  "openapi": "3.1.0",
  "info": {
    "title": "Lunonis Birth Chart API",
    "description": "Computes a real natal (birth) chart: Sun, Moon and rising signs, planet signs and houses, the tightest aspects and an element/modality summary. Read-only. Birth data is used for the calculation only and is not stored. The same calculation is also available as an MCP server (https://lunonis.com/mcp).",
    "version": "1.0.0"
  },
  "externalDocs": { "description": "Lunonis MCP server for Claude, ChatGPT and other AI apps", "url": "https://lunonis.com/developers/mcp" },
  "servers": [{ "url": "https://lunonis.com" }],
  "paths": {
    "/api/gpt/chart": {
      "get": {
        "operationId": "getBirthChart",
        "summary": "Compute a natal chart",
        "description": "Returns the natal chart for a birth date, optional birth time and a birth city. If the city name matches several places, the response has status \"ambiguous_place\" and a list of candidates: ask the user which one, then call again with that candidate's id as `place`. Use only the values returned here; never invent placements.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Birth date, YYYY-MM-DD (1900 to 2100).",
            "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "examples": ["1994-02-24"] }
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "description": "Local birth time, 24-hour HH:MM. Omit it if the user does not know it: then the rising sign and houses are not returned.",
            "schema": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "examples": ["08:12"] }
          },
          {
            "name": "place",
            "in": "query",
            "required": true,
            "description": "Birth city name (for example \"Lviv\" or \"Paris\") or a place id from searchPlaces or from an earlier ambiguous_place response.",
            "schema": { "type": "string", "maxLength": 80, "examples": ["Lviv", "702550"] }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language of the lunonis.com pages in the returned links.",
            "schema": { "type": "string", "enum": ["en", "es", "pt", "fr", "de", "it", "pl", "tr", "ja", "zh", "uk"], "default": "en" }
          }
        ],
        "responses": {
          "200": {
            "description": "The chart, or a list of candidate places to ask the user about.",
            "content": {
              "application/json": {
                "schema": { "oneOf": [{ "$ref": "#/components/schemas/Chart" }, { "$ref": "#/components/schemas/AmbiguousPlace" }] }
              }
            }
          },
          "400": { "description": "Invalid date, time or place.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "The place was not found. Try the city name alone.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Too many requests; retry after a minute.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/gpt/places": {
      "get": {
        "operationId": "searchPlaces",
        "summary": "Search birth places",
        "description": "Finds cities matching a name, biggest first. Use it to tell apart cities with the same name (for example Paris, France and Paris, Texas) before calling getBirthChart.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "City name, at least 2 characters.",
            "schema": { "type": "string", "minLength": 2, "maxLength": 80, "examples": ["Paris"] }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching places (empty when none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["places"],
                  "properties": { "places": { "type": "array", "items": { "$ref": "#/components/schemas/Place" } } }
                }
              }
            }
          },
          "400": { "description": "Query too short or too long.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Too many requests; retry after a minute.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Sign": {
        "type": "string",
        "enum": ["aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces"]
      },
      "Place": {
        "type": "object",
        "required": ["id", "name", "region", "country"],
        "properties": {
          "id": { "type": "integer", "description": "Place id to pass as `place`." },
          "name": { "type": "string" },
          "region": { "type": "string", "description": "State, province or similar." },
          "country": { "type": "string", "description": "Two-letter country code." }
        }
      },
      "AmbiguousPlace": {
        "type": "object",
        "required": ["status", "message", "candidates"],
        "properties": {
          "status": { "type": "string", "const": "ambiguous_place" },
          "message": { "type": "string" },
          "candidates": { "type": "array", "items": { "$ref": "#/components/schemas/Place" } }
        }
      },
      "Chart": {
        "type": "object",
        "required": ["birth", "bigThree", "planets", "topAspects", "summary", "notes", "links"],
        "properties": {
          "birth": {
            "type": "object",
            "required": ["date", "time", "place"],
            "properties": {
              "date": { "type": "string" },
              "time": { "type": ["string", "null"], "description": "Null when the birth time was not given." },
              "place": { "$ref": "#/components/schemas/Place" }
            }
          },
          "bigThree": {
            "type": "object",
            "required": ["sun", "moon", "rising"],
            "properties": {
              "sun": { "$ref": "#/components/schemas/Sign" },
              "moon": { "$ref": "#/components/schemas/Sign" },
              "rising": { "oneOf": [{ "$ref": "#/components/schemas/Sign" }, { "type": "null" }], "description": "Null without a birth time." }
            }
          },
          "midheaven": { "oneOf": [{ "$ref": "#/components/schemas/Sign" }, { "type": "null" }], "description": "Null without a birth time." },
          "planets": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["planet", "sign", "degree", "retrograde"],
              "properties": {
                "planet": { "type": "string", "enum": ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"] },
                "sign": { "$ref": "#/components/schemas/Sign" },
                "degree": { "type": "number", "description": "Degrees within the sign, 0 to 30." },
                "retrograde": { "type": "boolean" },
                "house": { "type": "integer", "minimum": 1, "maximum": 12, "description": "Only with a birth time." }
              }
            }
          },
          "topAspects": {
            "type": "array",
            "description": "The tightest aspects, most exact first.",
            "items": {
              "type": "object",
              "required": ["between", "aspect", "orbDegrees"],
              "properties": {
                "between": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 2, "description": "Two planets, or a planet and asc/mc." },
                "aspect": { "type": "string", "enum": ["conjunction", "sextile", "square", "trine", "opposition"] },
                "orbDegrees": { "type": "number", "description": "Distance from exact, in degrees." }
              }
            }
          },
          "summary": {
            "type": "object",
            "required": ["elements", "modalities", "dominantElement", "dominantModality"],
            "properties": {
              "elements": { "type": "object", "additionalProperties": { "type": "integer" }, "description": "Counts of fire, earth, air, water." },
              "modalities": { "type": "object", "additionalProperties": { "type": "integer" }, "description": "Counts of cardinal, fixed, mutable." },
              "dominantElement": { "type": "string", "enum": ["fire", "earth", "air", "water"] },
              "dominantModality": { "type": "string", "enum": ["cardinal", "fixed", "mutable"] }
            }
          },
          "notes": { "type": "array", "items": { "type": "string" }, "description": "Caveats to tell the user, for example an unknown birth time." },
          "links": {
            "type": "object",
            "required": ["fullFreeChart", "gift"],
            "properties": {
              "fullFreeChart": { "type": "string", "format": "uri", "description": "The full free chart on lunonis.com for these birth details." },
              "gift": { "type": "string", "format": "uri", "description": "The Lunonis gift page (personalized astrology books and reports)." }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "hint": { "type": "string" },
          "issues": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "message": { "type": "string" } } } }
        }
      }
    }
  }
}
