{
  "openapi": "3.1.0",
  "info": {
    "title": "Magical Chart calculation API",
    "version": "1.0.0",
    "summary": "Deterministic astronomical calculation. No account, no key, no language model.",
    "description": "Public read-only endpoints that compute a natal chart, a planetary-line map, or the local sidereal sky from a moment and a place. Every figure comes from astronomy-engine through lib/astro-engine.js, the same code that renders the pages on magicalchart.com, so a JSON answer and the matching page can never disagree. Positions are geocentric ecliptic, tropical zodiac. No birth detail is stored. No endpoint here calls a language model. Fair use applies: 60 requests a minute per address, shared across all three routes, answered with HTTP 429 and a Retry-After header. Terms and the one thing we ask of agents: https://www.magicalchart.com/for-agents",
    "contact": { "name": "Magical Chart", "url": "https://www.magicalchart.com/contact" },
    "license": { "name": "Free to use, attribution requested", "url": "https://www.magicalchart.com/for-agents" }
  },
  "servers": [{ "url": "https://www.magicalchart.com" }],
  "paths": {
    "/api/v1/natal": {
      "post": {
        "operationId": "computeNatalChart",
        "summary": "Natal chart from a birth moment and place",
        "description": "Returns the ten chart bodies plus the true lunar nodes in their signs and houses, the twelve house cusps, the Ascendant and the Midheaven. Houses are Whole Sign by default; pass houseSystem 'equal' for Equal House. No Placidus claim is made above 66.5 degrees of latitude, and a warning says so in the response.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/NatalRequest" },
              "example": { "timestamp": "1990-05-15T14:30:00Z", "latitude": 48.85, "longitude": 2.35, "houseSystem": "whole-sign" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The computed chart",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NatalResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "405": { "description": "This route is POST only" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/sky": {
      "get": {
        "operationId": "computePlanetaryLines",
        "summary": "Planetary lines on the world map for one moment",
        "description": "Returns the planetary lines of a chart projected onto the Earth, with the catalogued cities that fall under them. This is the calculation behind the astrocartography page. Cached for 24 hours.",
        "parameters": [
          { "name": "utc", "in": "query", "required": true, "description": "The moment, as an ISO 8601 string in UTC.", "schema": { "type": "string", "format": "date-time" }, "example": "1990-05-15T14:30:00Z" },
          { "name": "seuil", "in": "query", "required": false, "description": "How close a city must be to a line to be listed, in kilometres. Defaults to 400.", "schema": { "type": "number", "default": 400 } }
        ],
        "responses": {
          "200": { "description": "Lines and the cities under them", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Enveloppe" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/stars": {
      "get": {
        "operationId": "computeLocalSiderealSky",
        "summary": "Local sidereal time for a moment and a place",
        "description": "Returns Greenwich and local sidereal time, plus the resolved city, for one moment and one location. Give either a city name from the catalogue or an explicit latitude and longitude. This is what orients a star map. Cached for 24 hours.",
        "parameters": [
          { "name": "utc", "in": "query", "required": true, "description": "The moment, as an ISO 8601 string in UTC.", "schema": { "type": "string", "format": "date-time" }, "example": "1990-05-15T14:30:00Z" },
          { "name": "city", "in": "query", "required": false, "description": "City name, resolved against the bundled catalogue. Ignored when lat and lon are given.", "schema": { "type": "string" }, "example": "Paris" },
          { "name": "lat", "in": "query", "required": false, "description": "Latitude in degrees, -90 to 90.", "schema": { "type": "number" } },
          { "name": "lon", "in": "query", "required": false, "description": "Longitude in degrees, -180 to 180.", "schema": { "type": "number" } }
        ],
        "responses": {
          "200": { "description": "Sidereal time and the resolved place", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StarsResponse" } } } },
          "400": { "description": "Missing utc, or neither city nor lat and lon", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erreur" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "The input was rejected. The message says which field and why.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erreur" } } }
      },
      "RateLimited": {
        "description": "Fair-use limit reached: 60 a minute per address for anonymous callers. Wait the number of seconds in Retry-After and resume.",
        "headers": { "Retry-After": { "description": "Seconds to wait.", "schema": { "type": "integer" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Erreur" } } }
      }
    },
    "schemas": {
      "NatalRequest": {
        "type": "object",
        "required": ["timestamp", "latitude", "longitude"],
        "properties": {
          "timestamp": { "type": "string", "format": "date-time", "description": "Birth moment in UTC, ISO 8601. Convert from local time before calling: an hour of error moves the Ascendant by about 15 degrees." },
          "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
          "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
          "houseSystem": { "type": "string", "enum": ["whole-sign", "equal"], "default": "whole-sign" }
        }
      },
      "NatalResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": {
            "type": "object",
            "properties": {
              "birth": { "type": "object", "properties": { "timestamp": { "type": "string" }, "latitude": { "type": "number" }, "longitude": { "type": "number" } } },
              "planets": { "type": "array", "items": { "$ref": "#/components/schemas/Corps" } },
              "houses": { "type": "array", "items": { "type": "object", "properties": { "number": { "type": "integer" }, "cuspLongitude": { "type": "number" }, "sign": { "type": "object", "properties": { "name": { "type": "string" } } } } } },
              "houseSystem": { "type": "string" },
              "angles": { "type": "object", "properties": { "ascendant": { "type": "number" }, "midheaven": { "type": "number" } } },
              "warnings": { "type": "array", "items": { "type": "string" }, "description": "Non-empty above 66.5 degrees of latitude, where house systems stop behaving." }
            }
          },
          "provenance": { "$ref": "#/components/schemas/Provenance" }
        }
      },
      "Corps": {
        "type": "object",
        "description": "One chart body. The node names are 'true-north-node' and 'true-south-node', computed as the true node and not the mean one.",
        "properties": {
          "body": { "type": "string", "example": "sun" },
          "sign": { "type": "object", "properties": { "name": { "type": "string", "example": "Taurus" } } },
          "longitude": { "type": "number", "description": "Ecliptic longitude in degrees, 0 to 360." },
          "degree": { "type": "number", "description": "Degree within the sign, 0 to 30." },
          "house": { "type": ["integer", "null"] },
          "isRetrograde": { "type": "boolean" }
        }
      },
      "StarsResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": {
            "type": "object",
            "properties": {
              "utc": { "type": "string" },
              "city": { "type": "string" },
              "country": { "type": "string" },
              "latitude": { "type": "number" },
              "longitude": { "type": "number" },
              "gmst": { "type": "number", "description": "Greenwich mean sidereal time, in degrees." },
              "localSiderealTime": { "type": "number", "description": "Local sidereal time, in degrees." }
            }
          },
          "provenance": { "$ref": "#/components/schemas/Provenance" }
        }
      },
      "Enveloppe": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": { "type": "object" },
          "provenance": { "$ref": "#/components/schemas/Provenance" }
        }
      },
      "Provenance": {
        "type": "object",
        "description": "Present on every answer, so a caller can say where a figure came from.",
        "properties": {
          "engine": { "type": "string", "example": "magical-chart-astro-engine" },
          "route": { "type": "string" },
          "coordinateSystem": { "type": "string", "example": "geocentric ecliptic, tropical zodiac" },
          "houses": { "type": "string" }
        }
      },
      "Erreur": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "limit": { "type": "integer" },
          "window": { "type": "string" },
          "docs": { "type": "string" }
        }
      }
    }
  }
}
