{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/openephemeris-com/json-schema/openephemeris-com-acgrequest-payload-schema.json",
  "title": "ACGRequestPayload",
  "description": "Primary payload structure for ACG global line calculation requests.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/openephemeris-com-openapi.json#/components/schemas/ACGRequestPayload",
  "additionalProperties": false,
  "properties": {
    "bodies": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "maxItems": 128,
          "type": "array"
        }
      ],
      "description": "Explicit body name selection. Supported: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, Chiron, Pholus, Ceres, Pallas, Juno, Vesta, MeanNode, SouthNode, TrueNode, TrueSouthNode, MeanLilith, OscuLilith, Vertex, and the seven Hermetic lots Fortune, Spirit, Eros, Necessity, Courage, Victory and Nemesis. The eight Uranian (Hamburg School) hypothetical points (Cupido, Hades, Zeus, Kronos, Apollon, Admetos, Vulkanus, Poseidon) are accepted too, but only when named here: they are opt-in and never appear in a group's default roster. Fortune and Spirit are Hermetic lots and resolve only under a lot-bearing roster: `group: \"hermetic\"` or `group: \"all\"` (POST /acg/hermetic-lines pins that group itself, so it needs no `group`). Naming a lot under any other group is rejected with 422 rather than silently returning the default roster; on /acg/hits, where `hermetic`/`all` are Scale-tier, a lot named by a caller below that tier is rejected the same way. Aliases: NorthNode/Rahu/Node -> MeanNode; TrueNorthNode/TrueRahu -> TrueNode; Ketu -> SouthNode; Vulcanus -> Vulkanus. The extended catalog is accepted too, by name and only when named: the sb441 main-belt asteroids (Hygiea, Eunomia, Psyche, Europa (asteroid), Cybele, Sylvia, Thisbe, Camilla, Iris, Davida, Euphrosyne, Interamnia), the trans-Neptunian objects (Eris, Sedna, Makemake, Haumea, Quaoar, Orcus, Ixion, Varuna, Chaos, Gonggong, Salacia), the extra centaurs (Asbolus, Hylonome, Nessus, Chariklo) and about forty further asteroids (Eros (asteroid), Sappho, Lilith (asteroid), Fortuna, Pandora, Diana, Hekate and more) - GET /catalogs/bodies lists them all. The asteroid Eros is `Eros (asteroid)`: plain `Eros` is the Hermetic lot. Each extended body named adds 1 credit to the request (capped at +20); the standard roster, the Uranian set and the lots are covered by the base price. Extended bodies are never in a group's default roster.",
      "title": "Bodies"
    },
    "custom_bodies": {
      "description": "Enterprise tier. Caller-supplied fixed points drawn like bodies: each `{id, name?, longitude/latitude or ra_deg/dec_deg, metadata?}` yields MC/IC/AC/DC lines (and aspect lines) at the coordinates given, with `id` and `metadata` echoed back. TOP-LEVEL field, a sibling of `bodies`. Up to 32 points, 1 credit each (with extended catalog bodies, capped at +20 per request). See the ACGCustomBody schema. Below Enterprise the request is refused with 403.",
      "items": {
        "$ref": "#/$defs/ACGCustomBody"
      },
      "maxItems": 32,
      "title": "Custom Bodies",
      "type": [
        "array",
        "null"
      ]
    },
    "epoch": {
      "description": "ISO-8601 datetime of the birth or event. Either put the zone on the value itself as a 'Z' or ±HH:MM suffix (\"1987-07-15T09:01:00-05:00\"), or send the naive local wall-clock time (\"1987-07-15T09:01:00\") and name its zone in the sibling `timezone` field. A zone-less clock time with no `timezone` is rejected with 400 ambiguous_datetime and is NEVER read as UTC. A date-only value resolves to 12:00 UTC.",
      "title": "Epoch",
      "type": "string"
    },
    "format": {
      "default": "json",
      "description": "Response format. `llm` returns compact rows + schema in metadata; only honored by /acg/hits.",
      "title": "Format",
      "type": "string"
    },
    "group": {
      "description": "Body group selector. `standard` (default) = planets + asteroids; `hermetic` = Hermetic Lot lines (Fortune, Spirit, etc.); `all` = both; `hd` = the Human Design 13 (Sun, Earth, Moon, Mercury–Pluto, Mean Node, South Node) — pair with options.include_hd_gate for gate-labelled lines. The Hermetic lots are a Scale-tier feature: on the Developer-tier endpoints that accept this selector (/acg/hits, /acg/power-lines, /acg/ccg) a below-Scale caller has `hermetic`/`all` downgraded to the standard roster, and naming a lot in `bodies` is rejected with 422 rather than silently answered with the full roster. Hermetic lots require natal birthplace data.",
      "title": "Group",
      "type": [
        "string",
        "null"
      ]
    },
    "jd": {
      "anyOf": [
        {
          "format": "double",
          "type": "number"
        }
      ],
      "description": "Julian Day (alternative to epoch)",
      "title": "Jd"
    },
    "natal": {
      "$ref": "#/$defs/ACGNatalRequest",
      "description": "Natal origin points"
    },
    "options": {
      "$ref": "#/$defs/ACGOptionsRequest",
      "description": "Calculation behavior options"
    },
    "query_lat": {
      "description": "Latitude of the query point",
      "title": "Query Lat",
      "type": [
        "number",
        "null"
      ]
    },
    "query_lon": {
      "description": "Longitude of the query point",
      "title": "Query Lon",
      "type": [
        "number",
        "null"
      ]
    },
    "radius_deg": {
      "description": "Proximity radius in degrees",
      "title": "Radius Deg",
      "type": [
        "number",
        "null"
      ]
    },
    "target_longitudes": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ACGTargetLongitude"
          },
          "maxItems": 360,
          "type": "array"
        }
      ],
      "description": "TOP-LEVEL field — a sibling of `bodies` and `options`, NOT nested inside `options`. Nesting it under `options` is silently ignored (unknown fields are dropped, so you get HTTP 200 with zero lines and no validation error). Synthetic points defined by arbitrary ecliptic longitudes rather than real bodies — e.g. the 12 zodiac sign cusps for chart-rotation (\"relocation astrology\": where would this degree sit on the AC/MC). Each produces MC/IC/AC/DC line geometry via the same math as a real body, filtered by `options.line_types` (that one IS under options). Lines are identified by target_longitude/label in properties rather than body_name. AC/DC use the geometric horizon (h₀ = 0) regardless of options.refraction_correction: a target longitude is a mathematical point with no light path to refract, and this keeps the line locked to the same definition the house engine uses for the Ascendant.",
      "title": "Target Longitudes"
    },
    "timezone": {
      "$ref": "#/$defs/TimezoneInput",
      "description": "Zone that governs a naive (zone-less) `epoch`. Supply this and the server does the timezone math, including the historical-correction overlay for pre-1970 dates when `natal.birthplace_lat`/`birthplace_lon` are also present — a 1961 Minnesota birth resolves to CST where raw IANA tzdata says CDT. Ignored when `epoch` already carries a Z/±HH:MM suffix or when `jd` is used. The resolution is echoed back in the response `metadata` as tz_confidence / tz_rule_source / tz_resolved_offset / resolved_utc."
    }
  },
  "required": [
    "epoch"
  ],
  "type": "object",
  "$defs": {
    "ACGAsteroidGroup": {
      "description": "Categorical asteroid groups explicitly permitted for AstroCartography.",
      "enum": [
        "major",
        "centaur",
        "tno",
        "muse"
      ],
      "title": "ACGAsteroidGroup",
      "type": "string"
    },
    "ACGCustomBody": {
      "additionalProperties": false,
      "description": "A caller-supplied fixed point that is drawn exactly like a body: MC/IC/AC/DC lines (plus aspect lines and, on /acg/ccg, a per-snapshot score) at coordinates YOU give, rather than at a position looked up in the ephemeris. Use it for anything the catalog does not carry: a star, a planned transit position, a fixed point from another chart, a research point.\n\nGive the position in exactly one frame. ECLIPTIC: `longitude` (with optional `latitude`, default 0), on the tropical mean ecliptic of the chart date. EQUATORIAL: `ra_deg` and `dec_deg`, in degrees. Supplying both frames, or half of the equatorial pair, is rejected with 422.\n\nThe point is FIXED: it does not move with time. On /acg/ccg it holds the same coordinates at every snapshot.\n\n`id` is your stable identifier and is echoed on every line and in `metadata.bodies` as `custom_id`; `metadata` is an arbitrary JSON object (at most 2048 bytes serialized) echoed back verbatim as `metadata` on every line and in `metadata.bodies`. Neither is interpreted. `name` labels the point (default: `id`) and becomes `body_name`; it must not collide with a catalog body name or another custom point. Lines carry `category: \"custom\"`.\n\nEnterprise tier only (403 below it). Each custom point adds 1 credit to the request, like an extended catalog body (the surcharge is capped at 20 per request across both).",
      "properties": {
        "dec_deg": {
          "description": "Declination in degrees, -90 to 90. Equatorial frame: give with `ra_deg`.",
          "format": "double",
          "title": "Dec Deg",
          "type": [
            "number",
            "null"
          ]
        },
        "id": {
          "description": "Your identifier for this point: 1-64 characters, letters, digits and `_ . : -`, starting with a letter or digit. Unique within the request (case-insensitive). Echoed as `custom_id`.",
          "maxLength": 64,
          "minLength": 1,
          "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]*$",
          "title": "Id",
          "type": "string"
        },
        "latitude": {
          "description": "Ecliptic latitude in degrees, -90 to 90. Optional, default 0. Ecliptic frame only.",
          "format": "double",
          "title": "Latitude",
          "type": [
            "number",
            "null"
          ]
        },
        "longitude": {
          "description": "Ecliptic longitude in degrees, any real value (wrapped into [0, 360)). Ecliptic frame: give this and, optionally, `latitude`.",
          "format": "double",
          "title": "Longitude",
          "type": [
            "number",
            "null"
          ]
        },
        "metadata": {
          "additionalProperties": true,
          "description": "Arbitrary JSON object, echoed back verbatim on every line and in `metadata.bodies`. At most 2048 bytes serialized. Never interpreted.",
          "title": "Metadata",
          "type": [
            "object",
            "null"
          ]
        },
        "name": {
          "description": "Display label and `body_name` for this point. Defaults to `id`. Up to 64 characters.",
          "maxLength": 64,
          "title": "Name",
          "type": [
            "string",
            "null"
          ]
        },
        "ra_deg": {
          "description": "Right ascension in DEGREES (0-360, wrapped), not hours. Equatorial frame: give with `dec_deg`.",
          "format": "double",
          "title": "Ra Deg",
          "type": [
            "number",
            "null"
          ]
        }
      },
      "required": [
        "id"
      ],
      "title": "ACGCustomBody",
      "type": "object"
    },
    "ACGNatalRequest": {
      "additionalProperties": false,
      "description": "Natal geographical data for ACG offset calculation.",
      "properties": {
        "birthplace_alt_m": {
          "anyOf": [
            {
              "type": "number"
            }
          ],
          "title": "Birthplace Alt M"
        },
        "birthplace_lat": {
          "description": "Birthplace latitude. Both birthplace coordinates must be supplied together -- a natal object with an incomplete pair is rejected rather than defaulting to 0,0. Distinct from the analysis target coordinates on the enclosing payload.",
          "maximum": 90,
          "minimum": -90,
          "title": "Birthplace Lat",
          "type": "number"
        },
        "birthplace_lon": {
          "description": "Birthplace longitude. Both birthplace coordinates must be supplied together -- a natal object with an incomplete pair is rejected rather than defaulting to 0,0. Distinct from the analysis target coordinates on the enclosing payload.",
          "maximum": 180,
          "minimum": -180,
          "title": "Birthplace Lon",
          "type": "number"
        },
        "houses_system": {
          "default": "W",
          "title": "Houses System",
          "type": "string"
        }
      },
      "title": "ACGNatalRequest",
      "type": "object"
    },
    "ACGOptionsRequest": {
      "additionalProperties": false,
      "description": "Configuration options for AstroCartography calculations.",
      "properties": {
        "angle_epoch": {
          "description": "Decouple the rotational frame from the position epoch: body positions come from epoch/jd as usual, but ACG line angles are projected against this epoch's GMST. Accepts an ISO-8601 UTC datetime or a bare Julian Day. Empty/omitted = angles use the position epoch (default, unchanged). Use to overlay a secondary chart on a base frame — e.g. Human Design Design-register lines on the natal map (positions at the Design epoch, angles at the natal epoch), matching the hd_expression heatmap convention; also progressed/return overlays. Positions and obliquity stay on the position epoch, so every line is rigidly translated in longitude by the GMST difference.",
          "title": "Angle Epoch",
          "type": "string"
        },
        "aspect_angles": {
          "anyOf": [
            {
              "items": {
                "type": "number"
              },
              "maxItems": 32,
              "type": "array"
            }
          ],
          "description": "Angles to compute aspects for",
          "title": "Aspect Angles"
        },
        "asteroid_groups": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/ACGAsteroidGroup"
              },
              "maxItems": 16,
              "type": "array"
            }
          ],
          "description": "Filter by asteroid group. Main belt and fixed stars are explicitly prohibited.",
          "title": "Asteroid Groups"
        },
        "include_aspects": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "Include aspect lines",
          "title": "Include Aspects"
        },
        "include_hd_gate": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "Enrich every emitted line feature with the Human Design gate its body's ecliptic longitude carries (properties.hd_gate: gate/line/color/tone/base). Purely additive — does not change the body set or geometry. Default false. Combine with bodies:[\"Earth\"] or group:\"hd\" for the HD-13 body set.",
          "title": "Include HD Gate"
        },
        "include_hd_lot_gates": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "With include_hd_gate, also attach the Human Design gate to Hermetic Lot lines (Fortune, Spirit, and the rest of the lot suite), computed from the lot's own ecliptic longitude in the personality register. Lots are left out of HD enrichment by default; no effect without include_hd_gate. Purely additive. Default false.",
          "title": "Include HD Lot Gates"
        },
        "include_lines": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "CCG only: emit MC/IC/AC/DC GeoJSON line geometry per snapshot (identical to /acg/power-lines, with ccg_hybrid progression applied per body). Default false. No effect on /acg/natal-lines.",
          "title": "Include Lines"
        },
        "include_midpoints": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "Include planetary midpoint lines",
          "title": "Include Midpoints"
        },
        "include_parans": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "Include parans (latitude crossings)",
          "title": "Include Parans"
        },
        "line_types": {
          "anyOf": [
            {
              "items": {
                "type": "string"
              },
              "maxItems": 16,
              "type": "array"
            }
          ],
          "description": "Line types to include. Supported: MC, IC, AC, DC, ASP. Aliases: POWER -> MC+IC+AC+DC; ASPECT/SPECT -> ASP.",
          "title": "Line Types"
        },
        "paran_lat_cutoff": {
          "anyOf": [
            {
              "format": "double",
              "type": "number"
            }
          ],
          "description": "Maximum absolute latitude (in degrees) for paran crossing points. Crossings whose intersection latitude exceeds this value (north or south) are excluded from the response. When null or omitted, no cutoff is applied (current behavior preserved). Range: 0-90.",
          "title": "Paran Latitude Cutoff"
        },
        "refraction_correction": {
          "anyOf": [
            {
              "type": "boolean"
            }
          ],
          "description": "Apply atmospheric refraction to AC/DC",
          "title": "Refraction Correction"
        },
        "resolution": {
          "anyOf": [
            {
              "type": "integer"
            }
          ],
          "description": "Resolution slices for mapping",
          "title": "Resolution"
        }
      },
      "title": "ACGOptionsRequest",
      "type": "object"
    },
    "ACGTargetLongitude": {
      "additionalProperties": false,
      "description": "A synthetic point defined by an arbitrary ecliptic longitude rather than a resolved body position — e.g. a zodiac sign cusp for chart-rotation (\"relocation astrology\") queries. Produces the same MC/IC/AC/DC line geometry as a real body via ACGMCLine/ACGICLine/ACGACPath/ACGDCPath, with the longitude converted straight to RA/Dec instead of resolved from an ephemeris body.",
      "properties": {
        "label": {
          "description": "Display label for this point, e.g. \"Leo cusp\". Defaults to \"<Sign> cusp\" derived from longitude when omitted.",
          "title": "Label",
          "type": "string"
        },
        "longitude": {
          "anyOf": [
            {
              "format": "double",
              "type": "number"
            }
          ],
          "description": "Target ecliptic longitude in degrees, any real value (wrapped into [0, 360) internally). Deliberately NOT marked `required`: 0 is a legitimate value (0° Aries), so an omitted field must be rejected by the handler rather than silently read as 0.",
          "title": "Longitude"
        }
      },
      "title": "ACGTargetLongitude",
      "type": "object"
    },
    "TimezoneInput": {
      "additionalProperties": false,
      "description": "Timezone input using IANA names or UTC offsets.",
      "properties": {
        "fold_preference": {
          "description": "Which occurrence to use when the naive local datetime falls in a fall-back fold (a repeated hour): 'earlier' (default — first occurrence) or 'later'. Only meaningful for naive datetimes; the response reports datetime_status=ambiguous_fold either way so the ambiguity can be surfaced to the user.",
          "enum": [
            "earlier",
            "later"
          ],
          "title": "Fold Preference",
          "type": "string"
        },
        "iana_name": {
          "anyOf": [
            {
              "maxLength": 128,
              "type": "string"
            }
          ],
          "description": "IANA timezone name",
          "title": "Iana Name"
        },
        "time_standard": {
          "description": "How to interpret a naive local datetime when this zone is applied. 'auto' (default): resolve through the historical-correction overlay where one covers the region/era, then IANA tzdata. 'standard': force the zone's standard (non-daylight) offset — for callers who KNOW the recorded time was standard time regardless of what tzdata believes. 'daylight': force standard+1h. Forcing either marks the resolution tz_confidence=client_override. Ignored when the datetime carries its own Z/±HH:MM suffix or when utc_offset is used.",
          "enum": [
            "auto",
            "standard",
            "daylight"
          ],
          "title": "Time Standard",
          "type": "string"
        },
        "utc_offset": {
          "anyOf": [
            {
              "type": "number"
            }
          ],
          "description": "UTC offset in hours",
          "title": "Utc Offset"
        }
      },
      "title": "TimezoneInput",
      "type": "object"
    }
  }
}