{
  "openapi": "3.1.0",
  "info": {
    "title": "CycleCalcs Astronomy API",
    "version": "2.3.0",
    "description": "A read-only astronomy API that returns the named answer and the numbers behind it, with the frame, epoch, time scale and refraction model stated on every response. It reports angles, times and names, and attaches no meaning to any of them. Computed in real time with the MIT-licensed Astronomy Engine. This document is generated from the shipped route table (netlify/lib/v2/registry.mjs) by tools/generate-openapi.mjs, so it cannot list a route, a parameter or a requirement the API does not actually enforce. Computed astronomical output is unrestricted: use, store, redistribute and sell it without attribution. The one exception is GeoNames place data served by GET /v2/places and by any response that resolved a `place` parameter, which requires the credit line in meta.attribution; see GET /v2/attribution for the full source register.",
    "license": {
      "name": "Computed output unrestricted; third-party data credited per response",
      "url": "https://www.cyclecalcs.com/api/terms.html"
    },
    "termsOfService": "https://www.cyclecalcs.com/api/terms.html",
    "contact": {
      "name": "CycleCalcs",
      "email": "info@cyclecalcs.com",
      "url": "https://www.cyclecalcs.com/api.html"
    }
  },
  "servers": [
    {
      "url": "https://www.cyclecalcs.com"
    }
  ],
  "externalDocs": {
    "description": "Human-readable endpoint reference",
    "url": "https://www.cyclecalcs.com/api/reference.html"
  },
  "paths": {
    "/v2": {
      "get": {
        "operationId": "getDiscovery",
        "summary": "The discovery document: every endpoint, parameter, convention and limit",
        "description": "The discovery document: every endpoint, parameter, convention and limit. Formats: json. Example: GET /v2",
        "parameters": [
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/conventions": {
      "get": {
        "operationId": "getConventions",
        "summary": "Every convention, threshold, model and limit the API uses, as data",
        "description": "Every convention, threshold, model and limit the API uses, as data. Formats: json. Example: GET /v2/conventions",
        "parameters": [
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/conventions",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/conventions"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/enums": {
      "get": {
        "operationId": "getEnums",
        "summary": "Every enumerated value the API can emit or accept, as data",
        "description": "Every enumerated value the API can emit or accept, as data. Formats: json. Some fields are currently withheld: The meteor_showers set is withheld until the meteor catalog reduction clears its license review. Example: GET /v2/enums?set=moon_phase_names",
        "parameters": [
          {
            "name": "set",
            "in": "query",
            "required": false,
            "description": "Which /v2/enums set to return. Mutually exclusive with `endpoint`. Omit both for every set.",
            "schema": {
              "type": "string",
              "enum": [
                "ambiguous",
                "bodies",
                "body_aliases_refused",
                "cache_classes",
                "catalogs",
                "compass_points",
                "constellations",
                "cycle_families",
                "cycle_keys",
                "cycle_kinds",
                "day_anchors",
                "eclipse_kinds",
                "error_codes",
                "event_kinds",
                "formats",
                "frame_origin",
                "frames_parameter",
                "hemisphere",
                "include_tokens",
                "interval_kinds",
                "interval_reasons",
                "libration_favoured_limbs",
                "meteor_showers",
                "moon_phase_names",
                "moon_phase_names_south",
                "non_goals",
                "nonexistent",
                "position_origins",
                "query_modes",
                "refraction",
                "rights",
                "rise_set_event_kinds",
                "rise_set_special",
                "rise_set_statuses",
                "shape",
                "stars",
                "style",
                "sun_position_bases",
                "time_format",
                "timezone_sources",
                "verbosity",
                "visibility_phrases",
                "warning_codes",
                "zodiac_signs"
              ]
            },
            "example": "moon_phase_names"
          },
          {
            "$ref": "#/components/parameters/EndpointParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer. Some fields are withheld; see meta.not_computed.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/enums?set=moon_phase_names",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/enums?set=moon_phase_names"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/attribution": {
      "get": {
        "operationId": "getAttribution",
        "summary": "Every source behind every endpoint, with its license and what it obliges a caller to do",
        "description": "Every source behind every endpoint, with its license and what it obliges a caller to do. Formats: json. Example: GET /v2/attribution?endpoint=/v2/moon",
        "parameters": [
          {
            "$ref": "#/components/parameters/EndpointParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/attribution?endpoint=/v2/moon",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/attribution?endpoint=/v2/moon"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/positions": {
      "get": {
        "operationId": "getPositions",
        "summary": "Where every body is, in every frame, with refraction stated",
        "description": "Where every body is, in every frame, with refraction stated. Formats: json, csv. Example: GET /v2/positions?bodies=sun,moon&at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate, or a comma-separated list of up to 24 instants (at=t1,t2,...), each read by the rules below. A list returns data.series with one row per entry, in the order sent, and echoes query.mode list; it is capped on its length, not on the time between its entries, and is not paged. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-28T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of targets, up to 20: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, star:<name> or hr:<n> for a named star (one of 80: the 57 navigational stars of the Nautical Almanac, Polaris, and the IAU-named stars of V 3.0 or brighter within 10.3 deg of the ecliptic, listed at /v2/enums?set=stars; names match on letters and digits, so star:kaus_australis works; proper motion is applied to the date), or radec:<ra>,<dec>[,j2000] entries. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "sun,moon"
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "description": "A single target: one of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, star:<name> or hr:<n> for a named star (one of 80: the 57 navigational stars of the Nautical Almanac, Polaris, and the IAU-named stars of V 3.0 or brighter within 10.3 deg of the ecliptic, listed at /v2/enums?set=stars; names match on letters and digits, so star:kaus_australis works; proper motion is applied to the date), or radec:<ra>,<dec>[,j2000] for a fixed celestial point (right ascension in decimal hours, declination in decimal degrees). Mutually exclusive with `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "moon"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "frames",
            "in": "query",
            "required": false,
            "description": "Which coordinate frame families to include: minimal is of-date equatorial and horizontal only; standard adds equatorial J2000, true ecliptic-of-date, horizontal and distance; all adds ecliptic J2000 and galactic.",
            "schema": {
              "type": "string",
              "enum": [
                "minimal",
                "standard",
                "all"
              ],
              "default": "standard"
            },
            "example": "standard"
          },
          {
            "name": "frame",
            "in": "query",
            "required": false,
            "description": "Measured from the center of the Earth, or the center of the Sun. Earth itself has no geocentric position and needs frame=heliocentric.",
            "schema": {
              "type": "string",
              "enum": [
                "geocentric",
                "heliocentric"
              ],
              "default": "geocentric"
            },
            "example": "geocentric"
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/positions?bodies=sun,moon&at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/positions?bodies=sun,moon&at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/positions?bodies=sun,moon&at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/positions?bodies=sun,moon&at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/rise-set": {
      "get": {
        "operationId": "getRiseSet",
        "summary": "Rise, transit, set and lower transit for any named object, with an explicit status at every latitude",
        "description": "Rise, transit, set and lower transit for any named object, with an explicit status at every latitude. Formats: json, csv. Example: GET /v2/rise-set?body=moon&at=2026-12-21&lat=69.6492&lon=18.9553&tz=Europe/Oslo",
        "parameters": [
          {
            "name": "body",
            "in": "query",
            "required": true,
            "description": "A single target: one of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, star:<name> or hr:<n> for a named star (one of 80: the 57 navigational stars of the Nautical Almanac, Polaris, and the IAU-named stars of V 3.0 or brighter within 10.3 deg of the ecliptic, listed at /v2/enums?set=stars; names match on letters and digits, so star:kaus_australis works; proper motion is applied to the date), or radec:<ra>,<dec>[,j2000] for a fixed celestial point (right ascension in decimal hours, declination in decimal degrees). Mutually exclusive with `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "moon"
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of targets, up to 20: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, star:<name> or hr:<n> for a named star (one of 80: the 57 navigational stars of the Nautical Almanac, Polaris, and the IAU-named stars of V 3.0 or brighter within 10.3 deg of the ecliptic, listed at /v2/enums?set=stars; names match on letters and digits, so star:kaus_australis works; proper motion is applied to the date), or radec:<ra>,<dec>[,j2000] entries. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "sun,moon"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-12-21"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. On this endpoint `end` does not bound a row, it bounds a day. Every local day that BEGINS before `end` is returned, so the last row can begin before `end` and finish after it. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "$ref": "#/components/parameters/LatitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "horizon",
            "in": "query",
            "required": false,
            "description": "Overrides the rise/set event altitude, in degrees, replacing the standard -0.8333 deg (or the body's own definition). Disclosed with a warning.",
            "schema": {
              "type": "number",
              "minimum": -20,
              "maximum": 20
            },
            "example": -0.5
          },
          {
            "name": "search_horizon_days",
            "in": "query",
            "required": false,
            "description": "How far past the requested window to keep searching for the next real event, in days, when none falls inside the window (for example, through a polar night).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 400
            },
            "example": 200
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/rise-set?body=moon&at=2026-12-21&lat=69.6492&lon=18.9553&tz=Europe/Oslo",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/rise-set?body=moon&at=2026-12-21&lat=69.6492&lon=18.9553&tz=Europe/Oslo"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/rise-set?body=moon&at=2026-12-21&lat=69.6492&lon=18.9553&tz=Europe/Oslo&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/rise-set?body=moon&at=2026-12-21&lat=69.6492&lon=18.9553&tz=Europe/Oslo&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/sun": {
      "get": {
        "operationId": "getSun",
        "summary": "The complete solar day: every named boundary, azimuth, day length and polar status",
        "description": "The complete solar day: every named boundary, azimuth, day length and polar status. Formats: json, csv, txt. Example: GET /v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18",
        "parameters": [
          {
            "$ref": "#/components/parameters/LatitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-27"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. On this endpoint `end` does not bound a row, it bounds a day. Every local day that BEGINS before `end` is returned, so the last row can begin before `end` and finish after it. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride, narrower on this endpoint than the generic step range. Use a duration this endpoint accepts, such as 1d, 2d, 7d, 30d or 1y. This endpoint returns one row per local day, so the stride has to be a whole number of days; for a monthly cadence send 30d. Needs `start`.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "depressions",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of up to 8 custom solar depression angles in degrees, 0 to 30, in addition to the civil/nautical/astronomical bands.",
            "schema": {
              "type": "string"
            },
            "example": "17,18"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sun?lat=51.4778&lon=-0.0015&at=2026-07-27&tz=Europe/London&depressions=17,18&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/moon": {
      "get": {
        "operationId": "getMoon",
        "summary": "The Moon in render-ready form: phase, illumination, bright limb, libration",
        "description": "The Moon in render-ready form: phase, illumination, bright limb, libration. Formats: json, csv, txt. Example: GET /v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-27T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "name": "hemisphere",
            "in": "query",
            "required": false,
            "description": "Which hemisphere's convention to use (phase emoji, or season pairing). Defaults to south when lat is negative, north otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "north",
                "south"
              ]
            },
            "example": "north"
          },
          {
            "name": "next_phases",
            "in": "query",
            "required": false,
            "description": "How many upcoming quarter phases to list. Default 4.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 24
            },
            "example": 4
          },
          {
            "name": "style",
            "in": "query",
            "required": false,
            "description": "How the phase is rendered on format=txt: the bare name, the phase emoji, or a one-line sentence.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "emoji",
                "sentence"
              ],
              "default": "name"
            },
            "example": "name"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/moon?at=2026-07-27T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/time": {
      "get": {
        "operationId": "getTime",
        "summary": "Every time scale, sidereal time, solar time, the equation of time and the Julian dates",
        "description": "Every time scale, sidereal time, solar time, the equation of time and the Julian dates. Formats: json, csv. Example: GET /v2/time?at=2026-07-27T12:00:00Z&lat=51.4778&lon=-0.0015",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-27T12:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/time?at=2026-07-27T12:00:00Z&lat=51.4778&lon=-0.0015",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/time?at=2026-07-27T12:00:00Z&lat=51.4778&lon=-0.0015"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/time?at=2026-07-27T12:00:00Z&lat=51.4778&lon=-0.0015&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/time?at=2026-07-27T12:00:00Z&lat=51.4778&lon=-0.0015&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/seasons": {
      "get": {
        "operationId": "getSeasons",
        "summary": "Every equinox and solstice in a window, with the length of each season they bound, and on request the cross-quarters and all 24 solar terms",
        "description": "Every equinox and solstice in a window, with the length of each season they bound, and on request the cross-quarters and all 24 solar terms. Formats: json, csv. Example: GET /v2/seasons?start=2026-01-01&end=2027-01-01&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` is NOT returned: the window is [start, end). Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "hemisphere",
            "in": "query",
            "required": false,
            "description": "Which hemisphere's convention to use (phase emoji, or season pairing). Defaults to south when lat is negative, north otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "north",
                "south"
              ]
            },
            "example": "north"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set. solar_terms adds to the event list every instant the Sun's apparent ecliptic longitude reaches a multiple of 15 deg, 24 a year, and brings events and cross_quarters with it: the quarters and cross-quarters keep their kinds and the other sixteen are kind solar_term, each named by solar_longitude_deg.",
            "schema": {
              "type": "string",
              "enum": [
                "events",
                "seasons",
                "cross_quarters",
                "year_length",
                "solar_terms"
              ]
            },
            "example": "events"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/seasons?start=2026-01-01&end=2027-01-01&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/seasons?start=2026-01-01&end=2027-01-01&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/seasons?start=2026-01-01&end=2027-01-01&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/seasons?start=2026-01-01&end=2027-01-01&tz=Europe/London&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/apsides": {
      "get": {
        "operationId": "getApsides",
        "summary": "Every perigee, apogee, perihelion and aphelion in a window, with its distance",
        "description": "Every perigee, apogee, perihelion and aphelion in a window, with its distance. Formats: json, csv. Example: GET /v2/apsides?start=2026-01-01&end=2026-04-01&bodies=moon,earth",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` is NOT returned: the window is [start, end). Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-04-01"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "description": "A single target: one of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] for a fixed celestial point (right ascension in decimal hours, declination in decimal degrees). Mutually exclusive with `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "moon"
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of targets, up to 20: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] entries. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "moon,earth"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filter to the near extremum (perigee/perihelion), the far one (apogee/aphelion), or both.",
            "schema": {
              "type": "string",
              "enum": [
                "near",
                "far",
                "all"
              ],
              "default": "all"
            },
            "example": "all"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include; comma-separate multiple tokens. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "events",
                "extremes",
                "nearest_syzygy",
                "angular_size"
              ]
            },
            "example": "events,extremes"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/apsides?start=2026-01-01&end=2026-04-01&bodies=moon,earth",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/apsides?start=2026-01-01&end=2026-04-01&bodies=moon,earth"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/apsides?start=2026-01-01&end=2026-04-01&bodies=moon,earth&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/apsides?start=2026-01-01&end=2026-04-01&bodies=moon,earth&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/moon-nodes": {
      "get": {
        "operationId": "getMoonNodes",
        "summary": "Every crossing of the ecliptic by the Moon, the node longitudes and the eclipse seasons they define",
        "description": "Every crossing of the ecliptic by the Moon, the node longitudes and the eclipse seasons they define. Formats: json, csv. Example: GET /v2/moon-nodes?start=2026-01-01&end=2027-01-01",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` is NOT returned: the window is [start, end). Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "node_model",
            "in": "query",
            "required": false,
            "description": "Which lunar node model to report: the engine's true (osculating) node, the analytic mean node, or both side by side.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "mean",
                "both"
              ],
              "default": "true"
            },
            "example": "true"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filter to ascending crossings, descending crossings, or both.",
            "schema": {
              "type": "string",
              "enum": [
                "ascending",
                "descending",
                "all"
              ],
              "default": "all"
            },
            "example": "all"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "crossings",
                "node_longitude",
                "eclipse_seasons",
                "periods",
                "standstill"
              ]
            },
            "example": "crossings"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/moon-nodes?start=2026-01-01&end=2027-01-01",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/moon-nodes?start=2026-01-01&end=2027-01-01"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/moon-nodes?start=2026-01-01&end=2027-01-01&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/moon-nodes?start=2026-01-01&end=2027-01-01&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/eclipses": {
      "get": {
        "operationId": "getEclipses",
        "summary": "Solar and lunar eclipses with global geometry, local circumstances and an explicit answer to whether you can see it",
        "description": "Solar and lunar eclipses with global geometry, local circumstances and an explicit answer to whether you can see it. Formats: json, csv, txt. Some fields are currently withheld: The Saros MEMBER number is withheld: it needs a 223-row table of series-start epochs, which is catalog data, and the obvious source has not cleared a provenance check. The series number is closed-form and is published. Example: GET /v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-28T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` is NOT returned: the window is [start, end). Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "How many eclipses of EACH type to return with direction=next or direction=previous, 1 to 50, so count=3 with type=both returns up to six rows. Mutually exclusive with `end`. With `start` and no `end` the window is a fixed 366 days from `start`, and count is ignored with a parameter_ignored warning.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "example": 2
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Search forward from `at`, backward from `at`, or list every eclipse in [start, end). range is implied by sending `start`.",
            "schema": {
              "type": "string",
              "enum": [
                "next",
                "previous",
                "range"
              ],
              "default": "next"
            },
            "example": "next"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Restrict to solar eclipses, lunar eclipses, or both.",
            "schema": {
              "type": "string",
              "enum": [
                "solar",
                "lunar",
                "both"
              ],
              "default": "both"
            },
            "example": "both"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "visible_only",
            "in": "query",
            "required": false,
            "description": "When true, and a location is given, restrict results to eclipses that are actually visible from that location.",
            "schema": {
              "type": "boolean"
            },
            "example": false
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of optional blocks, any of global, local, saros, contacts, greatest, path. Omit it for the default set: global and saros, plus local and contacts when a location is given. A list that is sent REPLACES the default set. local and contacts need a location. greatest adds the circumstances at greatest local eclipse to the local block of a solar eclipse, so it needs a location and local. path adds the precomputed central path of a solar eclipse as inline GeoJSON where one exists, and says why for every other eclipse; the geometry travels only in nested JSON, and is left out, with a note, under format=csv or shape=flat or where it would push the response past the size limit. A token that cannot apply is ignored with a parameter_ignored warning. The pattern gives the canonical form: tokens are also accepted in any letter case, with spaces around the commas.",
            "schema": {
              "type": "string",
              "pattern": "^(global|local|saros|contacts|greatest|path)(,(global|local|saros|contacts|greatest|path))*$"
            },
            "example": "global,local"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer. Some fields are withheld; see meta.not_computed.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/eclipses?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&count=2&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/planet-board": {
      "get": {
        "operationId": "getPlanetBoard",
        "summary": "All eight planets in one request: place, brightness, apparent size, retrograde state, next station and whether it is worth looking tonight",
        "description": "All eight planets in one request: place, brightness, apparent size, retrograde state, next station and whether it is worth looking tonight. Formats: json, csv, txt. Example: GET /v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-28T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of targets, up to 20: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] entries. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "sun,moon"
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "description": "A single target: one of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] for a fixed celestial point (right ascension in decimal hours, declination in decimal degrees). Mutually exclusive with `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "moon"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "name": "frames",
            "in": "query",
            "required": false,
            "description": "Which coordinate frame families to include: minimal is of-date equatorial and horizontal only; standard adds equatorial J2000, true ecliptic-of-date, horizontal and distance; all adds ecliptic J2000 and galactic.",
            "schema": {
              "type": "string",
              "enum": [
                "minimal",
                "standard",
                "all"
              ],
              "default": "standard"
            },
            "example": "standard"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/planet-board?at=2026-07-28T21:30:00Z&lat=51.4778&lon=-0.0015&elevation_m=11&tz=Europe/London&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/retrogrades": {
      "get": {
        "operationId": "getRetrogrades",
        "summary": "Every retrograde and direct station of every planet in a window, with the loop length, its arc, and the interval of triple crossing",
        "description": "Every retrograde and direct station of every planet in a window, with the loop length, its arc, and the interval of triple crossing. Formats: json, csv. Example: GET /v2/retrogrades?body=mercury&start=2026-09-01&end=2026-12-31&include=periods,stations,triple_crossing,companion_event",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-12-31"
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "description": "A single target: one of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] for a fixed celestial point (right ascension in decimal hours, declination in decimal degrees). Mutually exclusive with `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "mercury"
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of targets, up to 20: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or radec:<ra>,<dec>[,j2000] entries. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "sun,moon"
          },
          {
            "name": "motion",
            "in": "query",
            "required": false,
            "description": "Filter stations by the motion they begin.",
            "schema": {
              "type": "string",
              "enum": [
                "retrograde",
                "direct",
                "all"
              ],
              "default": "all"
            },
            "example": "all"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "periods",
                "stations",
                "triple_crossing",
                "companion_event",
                "rate_curve"
              ]
            },
            "example": "periods,stations,triple_crossing,companion_event"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride, narrower on this endpoint than the generic step range. Use a duration this endpoint accepts, such as 1h, 12h, 1d, 7d or 30d. step sizes rate_curve only. The station scan stride is fixed per body and is not caller-tunable. Needs `start`.",
            "schema": {
              "type": "string"
            },
            "example": "1h"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/retrogrades?body=mercury&start=2026-09-01&end=2026-12-31&include=periods,stations,triple_crossing,companion_event",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/retrogrades?body=mercury&start=2026-09-01&end=2026-12-31&include=periods,stations,triple_crossing,companion_event"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/retrogrades?body=mercury&start=2026-09-01&end=2026-12-31&include=periods,stations,triple_crossing,companion_event&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/retrogrades?body=mercury&start=2026-09-01&end=2026-12-31&include=periods,stations,triple_crossing,companion_event&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/conjunctions": {
      "get": {
        "operationId": "getConjunctions",
        "summary": "Close approaches between body pairs in a window, ranked, with the separation and whether the pair is observable or lost in twilight",
        "description": "Close approaches between body pairs in a window, ranked, with the separation and whether the pair is observable or lost in twilight. Formats: json, csv. Example: GET /v2/conjunctions?bodies=venus,jupiter,mars&start=2026-01-01&end=2027-01-01&max_separation_deg=20",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of up to 8 bodies, whose every pair is searched: any of sun, moon, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto, or star:<name> or hr:<n> for a named star (one of 80: the 57 navigational stars of the Nautical Almanac, Polaris, and the IAU-named stars of V 3.0 or brighter within 10.3 deg of the ecliptic, listed at /v2/enums?set=stars; names match on letters and digits, so star:kaus_australis works; proper motion is applied to the date). A pair of two named stars is skipped, because two fixed stars never approach each other. A radec: target is refused here; /v2/separation takes one. Mutually exclusive with `pairs`.",
            "schema": {
              "type": "string"
            },
            "example": "venus,jupiter,mars"
          },
          {
            "name": "pairs",
            "in": "query",
            "required": false,
            "description": "An explicit list of body pairs to check, each written as two body names joined by a hyphen, separated by commas, for example jupiter-saturn,venus-mars or moon-star:regulus. Either half may be a named star, star:<name> or hr:<n>, never both halves. Overrides the all-pairs sweep of `bodies`.",
            "schema": {
              "type": "string"
            },
            "example": "jupiter-saturn"
          },
          {
            "name": "max_separation_deg",
            "in": "query",
            "required": false,
            "description": "Only report a minimum whose separation falls at or below this many degrees. Filters the result; never changes the search.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 180
            },
            "example": 20
          },
          {
            "name": "min_elongation_deg",
            "in": "query",
            "required": false,
            "description": "Only report a pair whose solar elongation is at or above this many degrees, so a conjunction lost in the Sun's glare can be excluded.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 180
            },
            "example": 0
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filter by how the approach is classified.",
            "schema": {
              "type": "string",
              "enum": [
                "conjunction",
                "close_approach",
                "all"
              ],
              "default": "all"
            },
            "example": "all"
          },
          {
            "name": "frame",
            "in": "query",
            "required": false,
            "description": "The frame the separation is measured in. ecliptic compares ecliptic longitude only, the almanac conjunction definition.",
            "schema": {
              "type": "string",
              "enum": [
                "apparent",
                "geocentric",
                "ecliptic"
              ],
              "default": "apparent"
            },
            "example": "apparent"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride, narrower on this endpoint than the generic step range. Use a duration this endpoint accepts, such as 1h, 6h, 1d, 2d or 7d. Needs `start`.",
            "schema": {
              "type": "string"
            },
            "example": "1h"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort order of the returned approaches.",
            "schema": {
              "type": "string",
              "enum": [
                "time",
                "separation"
              ],
              "default": "time"
            },
            "example": "time"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/conjunctions?bodies=venus,jupiter,mars&start=2026-01-01&end=2027-01-01&max_separation_deg=20",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/conjunctions?bodies=venus,jupiter,mars&start=2026-01-01&end=2027-01-01&max_separation_deg=20"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/conjunctions?bodies=venus,jupiter,mars&start=2026-01-01&end=2027-01-01&max_separation_deg=20&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/conjunctions?bodies=venus,jupiter,mars&start=2026-01-01&end=2027-01-01&max_separation_deg=20&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/separation": {
      "get": {
        "operationId": "getSeparation",
        "summary": "The angular separation between any two targets at an instant, and the minima of that separation across a window",
        "description": "The angular separation between any two targets at an instant, and the minima of that separation across a window. Formats: json, csv, txt. Example: GET /v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1",
        "parameters": [
          {
            "name": "a",
            "in": "query",
            "required": true,
            "description": "The first target: a body name, a named star (star:<name> or hr:<n>) or a radec: target, in the grammar of `bodies` above. Required.",
            "schema": {
              "type": "string"
            },
            "example": "jupiter"
          },
          {
            "name": "b",
            "in": "query",
            "required": true,
            "description": "The second target, in the same grammar as `a`, and different from it. Required.",
            "schema": {
              "type": "string"
            },
            "example": "saturn"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2020-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2021-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride, narrower on this endpoint than the generic step range. Use a duration this endpoint accepts, such as 1min, 15min, 6h, 1d or 7d. Needs `start`.",
            "schema": {
              "type": "string"
            },
            "example": "1min"
          },
          {
            "name": "max_separation_deg",
            "in": "query",
            "required": false,
            "description": "Only report a minimum whose separation falls at or below this many degrees. Filters the result; never changes the search.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 180
            },
            "example": 1
          },
          {
            "name": "frame",
            "in": "query",
            "required": false,
            "description": "The frame the separation is measured in. ecliptic compares ecliptic longitude only, for a named star or radec target as for a body.",
            "schema": {
              "type": "string",
              "enum": [
                "apparent",
                "geocentric",
                "ecliptic"
              ],
              "default": "apparent"
            },
            "example": "apparent"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/separation?a=jupiter&b=saturn&start=2020-01-01&end=2021-01-01&max_separation_deg=1&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/rarity": {
      "get": {
        "operationId": "getRarity",
        "summary": "How rare a pairing is: for two targets at an instant, the most recent earlier pairing that was at least as close and the next one, searched up to ten years each way; with a location, by default only pairings seen in a dark sky with both targets up count",
        "description": "How rare a pairing is: for two targets at an instant, the most recent earlier pairing that was at least as close and the next one, searched up to ten years each way; with a location, by default only pairings seen in a dark sky with both targets up count. Formats: json, csv, txt. Example: GET /v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York",
        "parameters": [
          {
            "name": "a",
            "in": "query",
            "required": true,
            "description": "The first target: a body name, a named star (star:<name> or hr:<n>) or a radec: target, in the grammar of `bodies` above. Required.",
            "schema": {
              "type": "string"
            },
            "example": "moon"
          },
          {
            "name": "b",
            "in": "query",
            "required": true,
            "description": "The second target, in the same grammar as `a`, and different from it. Required.",
            "schema": {
              "type": "string"
            },
            "example": "jupiter"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-08T09:56:30Z"
          },
          {
            "name": "frame",
            "in": "query",
            "required": false,
            "description": "The frame the separation is measured in. apparent is the true angle between the two apparent directions, topocentric when lat and lon are given; geocentric ignores the location. No ecliptic-longitude convention is offered here.",
            "schema": {
              "type": "string",
              "enum": [
                "apparent",
                "geocentric"
              ],
              "default": "apparent"
            },
            "example": "apparent"
          },
          {
            "name": "span_days",
            "in": "query",
            "required": false,
            "description": "How far the outward walk runs EACH WAY from `at`, in days, so the window searched is twice this. The default is the maximum, because \"closest in more than ten years\" is only true when the whole span was walked.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 3660,
              "default": 3660
            },
            "example": 3660
          },
          {
            "name": "exclude_days",
            "in": "query",
            "required": false,
            "description": "visible=any only: a separation minimum closer to `at` than this belongs to the pass being observed and is never reported as previous or next. Ignored, with a warning, under visible=dark.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 30,
              "default": 1.5
            },
            "example": 1.5
          },
          {
            "name": "visible",
            "in": "query",
            "required": false,
            "description": "Which pairings count. dark, the default with a location, frame=apparent and no Sun: only moments with both targets at least 3 degrees up, refracted, and the Sun at least 6 degrees down; the stretch `at` is in, both targets above the horizon in daylight or dark and holding one dark night (its own, or the nearer one), at most 18 hours either way, is never previous or next, and a closer moment inside it, in a dark sky, is returned as closer.before or closer.after. any, the default otherwise: every minimum of the separation, above the horizon or not. dark needs a location and frame=apparent and refuses the Sun.",
            "schema": {
              "type": "string",
              "enum": [
                "dark",
                "any"
              ]
            },
            "example": "dark"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/rarity?a=moon&b=jupiter&at=2026-09-08T09:56:30Z&lat=38.9696&lon=-77.3861&elevation_m=100&tz=America/New_York&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/cycles": {
      "get": {
        "operationId": "getCycles",
        "summary": "Where we are in every astronomical cycle the site tracks, with an explicit statement of whether each number was computed, observed, or is an interval with no live phase",
        "description": "Where we are in every astronomical cycle the site tracks, with an explicit statement of whether each number was computed, observed, or is an interval with no live phase. Formats: json, csv, txt. Example: GET /v2/cycles?at=2026-07-28T21:30:00Z",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-28T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "keys",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of specific cycle keys to return. Mutually exclusive with `kind`/`family`. Omit for every cycle.",
            "schema": {
              "type": "string",
              "enum": [
                "anomalistic_month",
                "anomalistic_year",
                "axial_precession",
                "callippic",
                "draconic_month",
                "eclipse_year",
                "exeligmos",
                "great_conjunction",
                "hale",
                "inex",
                "lunar_nodal_cycle",
                "lunar_standstill",
                "metonic_cycle",
                "saros",
                "sidereal_month",
                "sidereal_year",
                "sunspot_cycle",
                "synodic_month",
                "tritos",
                "tropical_year",
                "venus_pentagram"
              ]
            },
            "example": "synodic_month"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filter cycles by how their current position is known: computed from the ephemeris, observed (a live dataset), a named interval with no live phase, or a discrete state.",
            "schema": {
              "type": "string",
              "enum": [
                "computed_phase",
                "observed",
                "interval",
                "state"
              ]
            },
            "example": "computed_phase"
          },
          {
            "name": "family",
            "in": "query",
            "required": false,
            "description": "Filter to one cycle family. Mutually exclusive with `keys`.",
            "schema": {
              "type": "string",
              "enum": [
                "conjunction",
                "eclipse",
                "eclipse_period",
                "lunar_month",
                "node",
                "secular",
                "sunspot",
                "venus",
                "year"
              ]
            },
            "example": "conjunction"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "fraction",
                "next_event",
                "observed",
                "definition"
              ]
            },
            "example": "fraction,next_event"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/cycles?at=2026-07-28T21:30:00Z",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/cycles?at=2026-07-28T21:30:00Z"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/cycles?at=2026-07-28T21:30:00Z&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/cycles?at=2026-07-28T21:30:00Z&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/cycles?at=2026-07-28T21:30:00Z&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/cycles?at=2026-07-28T21:30:00Z&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/twilight": {
      "get": {
        "operationId": "getTwilight",
        "summary": "All three twilights plus day and night as non-overlapping intervals that tile the whole day, each with the reason it starts and ends, and a checksum the caller can assert",
        "description": "All three twilights plus day and night as non-overlapping intervals that tile the whole day, each with the reason it starts and ends, and a checksum the caller can assert. Formats: json, csv. Example: GET /v2/twilight?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-14"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. On this endpoint `end` does not bound a row, it bounds a day. Every local day that BEGINS before `end` is returned, so the last row can begin before `end` and finish after it. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Fixed at 1d: this endpoint answers a per-day question and accepts no other stride.",
            "schema": {
              "type": "string",
              "enum": [
                "1d"
              ]
            },
            "example": "1d"
          },
          {
            "$ref": "#/components/parameters/LatitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "depressions",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of up to 8 custom solar depression angles in degrees, 0 to 30, in addition to the civil/nautical/astronomical bands.",
            "schema": {
              "type": "string"
            },
            "example": "17,18"
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/twilight?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/twilight?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/twilight?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/twilight?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/dark-window": {
      "get": {
        "operationId": "getDarkWindow",
        "summary": "The genuinely dark moonless observing window of a night: astronomical night intersected with the Moon being down, ranked across up to 62 nights with a trend",
        "description": "The genuinely dark moonless observing window of a night: astronomical night intersected with the Moon being down, ranked across up to 62 nights with a trend. Formats: json, csv. Example: GET /v2/dark-window?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&nights=3",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-14"
          },
          {
            "$ref": "#/components/parameters/LatitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeRequiredParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "name": "nights",
            "in": "query",
            "required": false,
            "description": "How many consecutive local nights to rank, starting the night `at` falls in.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 62
            },
            "example": 3
          },
          {
            "name": "sun_depression_deg",
            "in": "query",
            "required": false,
            "description": "The solar depression angle, in degrees, that defines \"dark\" for this request. Default 18 (astronomical night).",
            "schema": {
              "type": "number",
              "minimum": 6,
              "maximum": 30
            },
            "example": 18
          },
          {
            "name": "moon_altitude_max_deg",
            "in": "query",
            "required": false,
            "description": "The Moon must be at or below this altitude, in degrees, to count as \"down\". Default 0.",
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 0
          },
          {
            "name": "moon_illumination_max",
            "in": "query",
            "required": false,
            "description": "Treat the Moon as \"down\" whenever its illuminated fraction is at or below this value, whatever its altitude. Default 1 (illumination never overrides altitude).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            },
            "example": 0.15
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/dark-window?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&nights=3",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/dark-window?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&nights=3"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/dark-window?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&nights=3&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/dark-window?lat=51.509&lon=-0.126&at=2026-11-14&tz=Europe/London&nights=3&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/sidereal-time": {
      "get": {
        "operationId": "getSiderealTime",
        "summary": "Greenwich and local sidereal time, apparent and mean side by side, with the equation of the equinoxes that separates them and the nutation terms it is built from",
        "description": "Greenwich and local sidereal time, apparent and mean side by side, with the equation of the equinoxes that separates them and the nutation terms it is built from. Formats: json, csv, txt. Example: GET /v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-14T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sidereal-time?lon=-0.126&lat=51.509&at=2026-11-14T21:30:00Z&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/equation-of-time": {
      "get": {
        "operationId": "getEquationOfTime",
        "summary": "How far a sundial runs ahead of or behind the clock, from the real Sun rather than a day-of-year series, with the Sun place that makes a range query an analemma",
        "description": "How far a sundial runs ahead of or behind the clock, from the real Sun rather than a day-of-year series, with the Sun place that makes a range query an analemma. Formats: json, csv, txt. Example: GET /v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-14T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "basic",
                "extremes"
              ]
            },
            "example": "basic"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/equation-of-time?lon=-0.126&at=2026-11-14T21:30:00Z&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/libration": {
      "get": {
        "operationId": "getLibration",
        "summary": "Lunar libration, the sub-Earth point, the axis and bright-limb position angles, and with the terminator token the sub-solar point and the colongitude that fixes where the terminator falls",
        "description": "Lunar libration, the sub-Earth point, the axis and bright-limb position angles, and with the terminator token the sub-solar point and the colongitude that fixes where the terminator falls. Formats: json, csv. Example: GET /v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-14T21:30:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride for a range query: a number plus a unit, one of min, m (minute), h (hour), d (day), w (week), mo (month) or y (year), for example 1h, 6h or 1d. Resolves to at least 1 minute and at most 1 year. Needs `start`. This is the generic grammar; an endpoint that narrows it documents the narrowing on its own `step` parameter, and /v2/conventions publishes every narrowing under limits.step_grammar.endpoint_policies.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include; terminator adds the sub-solar point and colongitude. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "basic",
                "terminator"
              ]
            },
            "example": "basic,terminator"
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/jupiter-moons": {
      "get": {
        "operationId": "getJupiterMoons",
        "summary": "Where the four Galilean moons are, and every transit, shadow transit, eclipse and occultation in the window, with both contact instants for each",
        "description": "Where the four Galilean moons are, and every transit, shadow transit, eclipse and occultation in the window, with both contact instants for each. Formats: json, csv. Example: GET /v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-15T09:40:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "step",
            "in": "query",
            "required": false,
            "description": "Sampling stride, 10 to 600 seconds (narrower than the generic step range: a Galilean contact needs finer control). A number plus s/sec or m/min.",
            "schema": {
              "type": "string",
              "enum": [
                "10s",
                "30s",
                "1m",
                "5m",
                "10m"
              ]
            },
            "example": "1m"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "moons",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of Galilean moons to include. Omit for all four.",
            "schema": {
              "type": "string",
              "enum": [
                "io",
                "europa",
                "ganymede",
                "callisto"
              ]
            },
            "example": "io,europa,ganymede,callisto"
          },
          {
            "name": "events",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of event families to include. Omit for all four.",
            "schema": {
              "type": "string",
              "enum": [
                "transit",
                "shadow",
                "eclipse",
                "occultation"
              ]
            },
            "example": "transit,shadow,eclipse,occultation"
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/sky-quality": {
      "get": {
        "operationId": "getSkyQuality",
        "summary": "Naked-eye limiting magnitude and aperture reach for a sky class you supply, with an explicit machine-readable statement that the number is a tuned estimate rather than a measurement",
        "description": "Naked-eye limiting magnitude and aperture reach for a sky class you supply, with an explicit machine-readable statement that the number is a tuned estimate rather than a measurement. Formats: json, csv, txt. Example: GET /v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200",
        "parameters": [
          {
            "name": "bortle",
            "in": "query",
            "required": false,
            "description": "The Bortle dark-sky class, 1 (excellent) to 9 (inner-city). Mutually exclusive with `nelm`; one of the two is required.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 9
            },
            "example": 3
          },
          {
            "name": "nelm",
            "in": "query",
            "required": false,
            "description": "Naked-eye limiting magnitude, 2.0 to 8.5. Mutually exclusive with `bortle`; one of the two is required.",
            "schema": {
              "type": "number",
              "minimum": 2,
              "maximum": 8.5
            },
            "example": 6
          },
          {
            "name": "equipment",
            "in": "query",
            "required": false,
            "description": "What the observation is made with.",
            "schema": {
              "type": "string",
              "enum": [
                "naked_eye",
                "binoculars",
                "telescope"
              ],
              "default": "naked_eye"
            },
            "example": "telescope"
          },
          {
            "name": "aperture_mm",
            "in": "query",
            "required": false,
            "description": "Telescope aperture in millimetres. Only used when equipment=telescope.",
            "schema": {
              "type": "number",
              "minimum": 5,
              "maximum": 5000
            },
            "example": 200
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "showpieces",
                "ladder",
                "sky_conditions"
              ]
            },
            "example": "ladder"
          },
          {
            "name": "refraction",
            "in": "query",
            "required": false,
            "description": "normal applies Saemundsson refraction to apparent altitude; none reports the unrefracted geometric altitude.",
            "schema": {
              "type": "string",
              "enum": [
                "normal",
                "none"
              ],
              "default": "normal"
            },
            "example": "normal"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/sky-quality?bortle=3&equipment=telescope&aperture_mm=200&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/places": {
      "get": {
        "operationId": "getPlaces",
        "summary": "Place-name search and reverse lookup against the self-hosted GeoNames index, returning name, region, country, coordinates and IANA timezone with a stable place_id",
        "description": "Place-name search and reverse lookup against the self-hosted GeoNames index, returning name, region, country, coordinates and IANA timezone with a stable place_id. Formats: json, csv. Every response is GeoNames data under CC BY 4.0, so every response carries rights: attribution_required and the credit in meta.attribution.text. The rest of the API is unrestricted. Example: GET /v2/places?q=cambridge&limit=5",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "A place name to search for, at least 2 characters after trimming. Mutually exclusive with `id` and with `lat`/`lon`.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 128
            },
            "example": "cambridge"
          },
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "An exact place_id from a previous /v2/places response, for example us-n35198-w111651. Mutually exclusive with `q` and with `lat`/`lon`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}-[NSns]\\d{5}-[EWew]\\d{6}$"
            },
            "example": "gb-n52125-w000008"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Restricts a name search to one country: an ISO 3166-1 alpha-2 code (case-insensitive) or a country name.",
            "schema": {
              "type": "string"
            },
            "example": "GB"
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "description": "Restricts a name search to one admin region (state or province name) as stored in the index.",
            "schema": {
              "type": "string"
            },
            "example": "Cambridgeshire"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of place matches to return, 1 to 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 6
            },
            "example": 5
          },
          {
            "name": "min_population",
            "in": "query",
            "required": false,
            "description": "Only return places at or above this population. The index stores population to the nearest 1,000, so the value is rounded up to the next thousand.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 40000000
            },
            "example": 100000
          },
          {
            "name": "radius_km",
            "in": "query",
            "required": false,
            "description": "Search radius in kilometres for a reverse (lat/lon) lookup. Default 250.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 2000
            },
            "example": 25
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer. Every response is GeoNames data under CC BY 4.0, so every response carries rights: attribution_required and the credit in meta.attribution.text. The rest of the API is unrestricted.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/places?q=cambridge&limit=5",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/places?q=cambridge&limit=5"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/places?q=cambridge&limit=5&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/places?q=cambridge&limit=5&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/phases": {
      "get": {
        "operationId": "getPhases",
        "summary": "The lunar phase calendar: every quarter with its distance, apparent size, supermoon classification under BOTH competing rules, traditional name and any eclipse",
        "description": "The lunar phase calendar: every quarter with its distance, apparent size, supermoon classification under BOTH competing rules, traditional name and any eclipse. Formats: json, csv, txt. Example: GET /v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-29"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` IS returned: the window is [start, end]. Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-08-31"
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of rows to return from a range query, counted forward from `start`. Mutually exclusive with `end`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "example": 30
          },
          {
            "name": "phases",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of quarter names to restrict the calendar to. Omit for all four.",
            "schema": {
              "type": "string",
              "enum": [
                "new_moon",
                "first_quarter",
                "full_moon",
                "last_quarter"
              ]
            },
            "example": "full_moon"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include. Omit for the endpoint default set.",
            "schema": {
              "type": "string",
              "enum": [
                "distance",
                "classification",
                "names",
                "eclipse",
                "libration",
                "rise_set"
              ]
            },
            "example": "distance,classification"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London&format=csv"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/phases?start=2026-07-29&end=2026-08-31&tz=Europe/London&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/planet-events": {
      "get": {
        "operationId": "getPlanetEvents",
        "summary": "Planet events as dated instants: the Mercury and Venus apparition cycle with its current state, the conjunctions with the Sun, quadratures and oppositions of Mars to Neptune, and IAU constellation entries for all seven",
        "description": "Planet events as dated instants: the Mercury and Venus apparition cycle with its current state, the conjunctions with the Sun, quadratures and oppositions of Mars to Neptune, and IAU constellation entries for all seven. Formats: json, csv. Example: GET /v2/planet-events?bodies=venus,mercury&start=2026-01-01&end=2027-01-01&lat=51.4778&lon=-0.0015&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-21T21:00:00Z"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "The start of a range query, inclusive. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "The end of a range query. A row landing exactly on `end` is NOT returned: the window is [start, end). Every endpoint's rule is published as data under limits.range_boundaries in /v2/conventions. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2027-01-01"
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "description": "A single planet: one of mercury, venus, mars, jupiter, saturn, uranus, neptune. Mutually exclusive with `bodies`. The Sun, the Moon, Pluto, named stars and radec: targets are refused with BODY_NOT_SUPPORTED_HERE.",
            "schema": {
              "type": "string"
            },
            "example": "mars"
          },
          {
            "name": "bodies",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of planets, any of mercury, venus, mars, jupiter, saturn, uranus, neptune. Default venus,mercury. Mutually exclusive with `body`.",
            "schema": {
              "type": "string"
            },
            "example": "venus,mercury"
          },
          {
            "name": "kinds",
            "in": "query",
            "required": false,
            "description": "A comma-separated list of event kinds, any of inferior_conjunction, superior_conjunction, greatest_elongation_east, greatest_elongation_west, peak_magnitude, transit_of_mercury, transit_of_venus, conjunction_with_sun, quadrature_west, opposition, quadrature_east, constellation_entry. Omit it for each body's own family: the seven apparition kinds for Mercury and Venus, and conjunction_with_sun, quadrature_west, opposition and quadrature_east for Mars to Neptune, the instants the planet's apparent geocentric ecliptic longitude minus the Sun's reaches 0, 270, 180 and 90 deg. constellation_entry, each crossing of an IAU constellation boundary, applies to every planet and is returned only when listed. A kind none of the requested bodies can have is ignored with a parameter_ignored warning.",
            "schema": {
              "type": "string"
            },
            "example": "opposition,constellation_entry"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/CursorParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/planet-events?bodies=venus,mercury&start=2026-01-01&end=2027-01-01&lat=51.4778&lon=-0.0015&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/planet-events?bodies=venus,mercury&start=2026-01-01&end=2027-01-01&lat=51.4778&lon=-0.0015&tz=Europe/London"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "A CSV rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/planet-events?bodies=venus,mercury&start=2026-01-01&end=2027-01-01&lat=51.4778&lon=-0.0015&tz=Europe/London&format=csv",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/planet-events?bodies=venus,mercury&start=2026-01-01&end=2027-01-01&lat=51.4778&lon=-0.0015&tz=Europe/London&format=csv"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v2/today": {
      "get": {
        "operationId": "getToday",
        "summary": "The whole sky for a place right now, in one request",
        "description": "The whole sky for a place right now, in one request. Formats: json, txt. Some fields are currently withheld: Meteor-shower events are withheld from next_events until the meteor catalog reduction clears its license review. Example: GET /v2/today?at=2026-07-28T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "The instant to evaluate. ISO 8601: a date (2026-06-21), or a date and time with an optional seconds, fraction and either Z or a numeric UTC offset (+hh:mm, no seconds). No offset and no `tz` reads as UTC. Or jd: followed by a Julian Day on the UT scale, read as UTC whatever `tz` says: jd:2461000.5 is 2025-11-21T00:00:00Z. Reliable from 1700 to 2200.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-28T21:00:00Z"
          },
          {
            "$ref": "#/components/parameters/LatitudeParam"
          },
          {
            "$ref": "#/components/parameters/LongitudeParam"
          },
          {
            "$ref": "#/components/parameters/ElevationMParam"
          },
          {
            "$ref": "#/components/parameters/PlaceParam"
          },
          {
            "$ref": "#/components/parameters/PlaceStrategyParam"
          },
          {
            "$ref": "#/components/parameters/TzParam"
          },
          {
            "$ref": "#/components/parameters/AmbiguousParam"
          },
          {
            "$ref": "#/components/parameters/NonexistentParam"
          },
          {
            "$ref": "#/components/parameters/DayAnchorParam"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "See the endpoint description at /api/reference.html.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/TimeFormatParam"
          },
          {
            "$ref": "#/components/parameters/FormatParam"
          },
          {
            "$ref": "#/components/parameters/FieldsParam"
          },
          {
            "$ref": "#/components/parameters/ShapeParam"
          },
          {
            "$ref": "#/components/parameters/PrecisionParam"
          },
          {
            "$ref": "#/components/parameters/PrettyParam"
          },
          {
            "$ref": "#/components/parameters/VerbosityParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The answer. Some fields are withheld; see meta.not_computed.",
            "headers": {
              "ETag": {
                "description": "Present when the cache class is deterministic or time-quantized. Send back as If-None-Match to get 304.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Browser caching directive for this response's cache class.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "draft-ietf-httpapi-ratelimit-headers usage for the current window.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/today?at=2026-07-28T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/today?at=2026-07-28T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A TXT rendering of the same answer. See meta and the CSV header comment for provenance."
                },
                "examples": {
                  "live": {
                    "summary": "GET /v2/today?at=2026-07-28T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=txt",
                    "description": "A real request to the running API, not a hand-written mock. It needs no key and can be opened directly, so the example is whatever the endpoint actually returns today.",
                    "externalValue": "https://www.cyclecalcs.com/v2/today?at=2026-07-28T21:00:00Z&lat=51.4778&lon=-0.0015&tz=Europe/London&format=txt"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not Modified. Returned when If-None-Match matches the current ETag. No body."
          },
          "400": {
            "description": "The request could not be understood: a malformed or out-of-range parameter, an incomplete range, a conflicting pair of parameters, or a request too large for the compute budget.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not found: a `place`/`id`/target that does not resolve to anything, or a path this API does not serve.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. This API serves GET, HEAD and OPTIONS only.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The serialized response exceeded the hard size cap. Narrow the request with `fields`, verbosity=compact or a smaller range.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait the number of seconds in Retry-After, or cache the response.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {},
    {
      "RapidAPIKey": []
    }
  ],
  "components": {
    "parameters": {
      "FormatParam": {
        "name": "format",
        "in": "query",
        "required": false,
        "description": "Response representation. Each endpoint serves a subset of json, csv and txt; see that operation's description for the exact list, or GET /v2 for every endpoint's formats array. An unsupported value is 400 BAD_FORMAT with the accepted list in `supported`.",
        "schema": {
          "type": "string"
        },
        "example": "json"
      },
      "PrecisionParam": {
        "name": "precision",
        "in": "query",
        "required": false,
        "description": "Decimal places for numeric fields.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 9,
          "default": 6
        },
        "example": 6
      },
      "PrettyParam": {
        "name": "pretty",
        "in": "query",
        "required": false,
        "description": "Whether the JSON body is indented (1) or compact (0). Has no effect on csv or txt.",
        "schema": {
          "type": "integer",
          "enum": [
            0,
            1
          ],
          "default": 1
        },
        "example": 1
      },
      "VerbosityParam": {
        "name": "verbosity",
        "in": "query",
        "required": false,
        "description": "full includes prose notes, frame definitions and withheld-field reasons in meta. compact keeps every value and every machine-readable name and drops the prose.",
        "schema": {
          "type": "string",
          "enum": [
            "full",
            "compact"
          ],
          "default": "full"
        },
        "example": "full"
      },
      "ShapeParam": {
        "name": "shape",
        "in": "query",
        "required": false,
        "description": "nested returns the documented object tree. flat returns one object with dotted keys, format=json only.",
        "schema": {
          "type": "string",
          "enum": [
            "nested",
            "flat"
          ],
          "default": "nested"
        },
        "example": "nested"
      },
      "TimeFormatParam": {
        "name": "time_format",
        "in": "query",
        "required": false,
        "description": "How instants are rendered: iso for ISO 8601 strings carrying the effective offset, or unix for integer seconds since the epoch. An ISO offset has no seconds field, so an instant in a pre-1972 local mean time era whose offset has seconds (Moscow +02:30:17 until 1916) is written in UTC with a Z; utc_offset_seconds stays exact, utc_offset_iso rounds it to the nearest minute, and the tzdb_lmt_interval warning says so.",
        "schema": {
          "type": "string",
          "enum": [
            "iso",
            "unix"
          ],
          "default": "iso"
        },
        "example": "iso"
      },
      "FieldsParam": {
        "name": "fields",
        "in": "query",
        "required": false,
        "description": "Comma-separated dot paths into `data` (for example moon.phase_name,moon.illumination) so only those fields are returned. Up to 64 paths; an array element is addressed by its object key, never by index.",
        "schema": {
          "type": "string"
        }
      },
      "LimitParam": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum rows to return from a range query, for cursor pagination. The per-endpoint cap is published at /v2/conventions; omit it to take everything the response size cap allows. /v2/places uses `limit` differently: see that operation.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "CursorParam": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "An opaque, unsigned pagination token copied verbatim from a previous response's links.next. Never construct one by hand; a malformed or foreign cursor is 400 BAD_CURSOR.",
        "schema": {
          "type": "string"
        }
      },
      "ElevationMParam": {
        "name": "elevation_m",
        "in": "query",
        "required": false,
        "description": "Observer elevation above mean sea level, in metres. Raises the observer on a locally flat plateau; sea-horizon dip is not modeled.",
        "schema": {
          "type": "number",
          "minimum": -500,
          "maximum": 9000,
          "default": 0
        },
        "example": 11
      },
      "TzParam": {
        "name": "tz",
        "in": "query",
        "required": false,
        "description": "An IANA time zone name, for example Europe/London or America/Chicago. Numeric offsets and abbreviations are not accepted. Governs how a date-only or offset-free `at`/`start`/`end` is read, and how local instants are rendered when time_format=iso. Before 1970 the tz database records the history of a zone for its principal location only, and some names are links to another zone: the tz_source_boundary_1970 warning names whose history was used, and tzdb_lmt_interval marks a local mean time. Neither changes an offset.",
        "schema": {
          "type": "string"
        },
        "example": "Europe/London"
      },
      "AmbiguousParam": {
        "name": "ambiguous",
        "in": "query",
        "required": false,
        "description": "How to resolve a local time that a clock repeated, such as the return from daylight saving. error refuses with 409 AMBIGUOUS_LOCAL_TIME.",
        "schema": {
          "type": "string",
          "enum": [
            "first",
            "last",
            "error"
          ],
          "default": "first"
        },
        "example": "first"
      },
      "NonexistentParam": {
        "name": "nonexistent",
        "in": "query",
        "required": false,
        "description": "How to resolve a local time that never happened, such as inside a spring-forward gap. error refuses with 400 NONEXISTENT_LOCAL_TIME.",
        "schema": {
          "type": "string",
          "enum": [
            "error",
            "shift_forward",
            "shift_back"
          ],
          "default": "error"
        },
        "example": "error"
      },
      "DayAnchorParam": {
        "name": "day_anchor",
        "in": "query",
        "required": false,
        "description": "civil: Local midnight in the effective IANA zone, with the historical offset for that date. solar: Local mean solar midnight implied by longitude alone. No timezone, no DST. utc: UTC midnight of the resolved UTC calendar date.",
        "schema": {
          "type": "string",
          "enum": [
            "civil",
            "solar",
            "utc"
          ]
        },
        "example": "civil"
      },
      "PlaceParam": {
        "name": "place",
        "in": "query",
        "required": false,
        "description": "A place name (place=London), a name with a country (place=Cambridge,GB), or a stable identifier from GET /v2/places (place=id:us-n35198-w111651). Supplies lat, lon and, unless tz is also sent, the time zone. Mutually exclusive with sending lat and lon directly. Withheld unless GET /v2 reports capabilities.place_lookup true.",
        "schema": {
          "type": "string"
        },
        "example": "London"
      },
      "PlaceStrategyParam": {
        "name": "place_strategy",
        "in": "query",
        "required": false,
        "description": "best guesses the intended place when a name is ambiguous and reports the guess as a warning. error refuses an ambiguous name with 400 AMBIGUOUS_PLACE and the candidates in `supported`. Only meaningful with `place`.",
        "schema": {
          "type": "string",
          "enum": [
            "best",
            "error"
          ],
          "default": "best"
        },
        "example": "best"
      },
      "EndpointParam": {
        "name": "endpoint",
        "in": "query",
        "required": false,
        "description": "A v2 endpoint path (or its bare slug), to scope the response to that one endpoint.",
        "schema": {
          "type": "string",
          "enum": [
            "/v2",
            "/v2/conventions",
            "/v2/enums",
            "/v2/attribution",
            "/v2/positions",
            "/v2/rise-set",
            "/v2/sun",
            "/v2/moon",
            "/v2/time",
            "/v2/seasons",
            "/v2/apsides",
            "/v2/moon-nodes",
            "/v2/eclipses",
            "/v2/planet-board",
            "/v2/retrogrades",
            "/v2/conjunctions",
            "/v2/separation",
            "/v2/rarity",
            "/v2/cycles",
            "/v2/twilight",
            "/v2/dark-window",
            "/v2/sidereal-time",
            "/v2/equation-of-time",
            "/v2/libration",
            "/v2/jupiter-moons",
            "/v2/sky-quality",
            "/v2/places",
            "/v2/phases",
            "/v2/planet-events",
            "/v2/today"
          ]
        },
        "example": "/v2/moon"
      },
      "LatitudeParam": {
        "name": "lat",
        "in": "query",
        "required": false,
        "description": "Latitude in decimal degrees, north positive. No compass suffix (do not append N or S). Required together with lon, or neither.",
        "schema": {
          "type": "number",
          "minimum": -90,
          "maximum": 90
        },
        "example": 51.4778
      },
      "LongitudeParam": {
        "name": "lon",
        "in": "query",
        "required": false,
        "description": "Longitude in decimal degrees, east positive. No compass suffix (do not append E or W). Required together with lat, or neither.",
        "schema": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
        },
        "example": -0.0015
      },
      "LatitudeRequiredParam": {
        "name": "lat",
        "in": "query",
        "required": true,
        "description": "Latitude in decimal degrees, north positive. No compass suffix (do not append N or S). Required together with lon, or neither.",
        "schema": {
          "type": "number",
          "minimum": -90,
          "maximum": 90
        },
        "example": 51.4778
      },
      "LongitudeRequiredParam": {
        "name": "lon",
        "in": "query",
        "required": true,
        "description": "Longitude in decimal degrees, east positive. No compass suffix (do not append E or W). Required together with lat, or neither.",
        "schema": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
        },
        "example": -0.0015
      }
    },
    "schemas": {
      "TropicalSign": {
        "type": "object",
        "description": "The 30-degree sector of the body's apparent ecliptic longitude in the true ecliptic and equinox of date (frame ect), counted from the moving March equinox: Aries 0 to 30 deg, Taurus 30 to 60 deg, and so on to Pisces 330 to 360 deg. A named coordinate bin, not a constellation: because the equinox precesses, the bins drift against the stars by about 1 deg every 71.6 years, at the measured IAU 2006 precession rate. Taken from the geocentric place on /v2/positions, /v2/moon and /v2/planet-board, even when lat and lon make a position block topocentric: /v2/positions says so in meta.conventions.label_origin and /v2/planet-board in position.ecliptic_origin. With a location the Moon's topocentric longitude can sit up to about 1 deg from the geocentric one its sector is taken from. On /v2/sun, which always has a location, it is taken from the topocentric place, at most about 9 arcsec from the geocentric one. Emitted for Solar System bodies only.",
        "required": [
          "name",
          "code",
          "sign_index",
          "degree_in_sign",
          "degree_in_sign_whole"
        ],
        "properties": {
          "name": {
            "type": "string",
            "enum": [
              "Aries",
              "Taurus",
              "Gemini",
              "Cancer",
              "Leo",
              "Virgo",
              "Libra",
              "Scorpio",
              "Sagittarius",
              "Capricorn",
              "Aquarius",
              "Pisces"
            ],
            "description": "The sector name."
          },
          "code": {
            "type": "string",
            "enum": [
              "aries",
              "taurus",
              "gemini",
              "cancer",
              "leo",
              "virgo",
              "libra",
              "scorpio",
              "sagittarius",
              "capricorn",
              "aquarius",
              "pisces"
            ],
            "description": "The sector name as a stable lower-case code."
          },
          "sign_index": {
            "type": "integer",
            "minimum": 0,
            "maximum": 11,
            "description": "The sector number, 0 for 0 to 30 deg through 11 for 330 to 360 deg."
          },
          "degree_in_sign": {
            "type": "number",
            "minimum": 0,
            "maximum": 30,
            "description": "Degrees of longitude past the start of the sector, from 0 up to but not including 30. The bound is written as a plain maximum so the OpenAPI 3.0 copy stays valid."
          },
          "degree_in_sign_whole": {
            "type": "integer",
            "minimum": 0,
            "maximum": 29,
            "description": "degree_in_sign truncated, never rounded."
          }
        }
      },
      "Constellation": {
        "type": "object",
        "description": "The IAU constellation containing the position: one of the 88 regions whose boundaries Delporte drew for the IAU in 1930 along lines of right ascension and declination of the B1875.0 equinox. The J2000 right ascension and declination are precessed to B1875.0 and looked up there. A body off the ecliptic can sit outside the zodiac constellations, so the constellation and tropical_sign often disagree: Saturn at 2026-10-04T12:00:00Z is at ecliptic longitude 11.30 deg and latitude -2.71 deg, in the Aries bin of tropical_sign and in the constellation Cetus. Taken from the geocentric place on /v2/positions, /v2/moon, /v2/today, /v2/retrogrades and /v2/planet-events, even when lat and lon make a position block topocentric, and on /v2/positions from the heliocentric place under frame=heliocentric. On /v2/planet-board (with a location, unless frames=minimal) and /v2/sun it is taken from the topocentric place, which sits at most the body's parallax from the geocentric one: about 9 arcsec for the Sun and 33 arcsec for Venus at its closest. On /v2/positions it is also given for named star and radec targets.",
        "required": [
          "name",
          "abbreviation"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The IAU constellation name, nominative."
          },
          "abbreviation": {
            "type": "string",
            "description": "The three-letter IAU abbreviation."
          }
        }
      },
      "LabelOrigin": {
        "type": "string",
        "enum": [
          "geocentric",
          "heliocentric"
        ],
        "description": "On /v2/positions, meta.conventions.label_origin: the place every row's tropical_sign and constellation were taken from, one value for the whole response, present when some row carries either label. It is geocentric, except that under frame=heliocentric the constellation is taken from the heliocentric place and label_origin says heliocentric, and no tropical_sign is emitted. Never topocentric, even when a row's position block is. A named star or radec row has a constellation and no tropical_sign."
      },
      "Envelope": {
        "type": "object",
        "description": "The nine-key v2 response envelope. Every /v2 endpoint returns exactly these keys, in this order, on every 200 (spec-00 1.1).",
        "required": [
          "endpoint",
          "computed_at",
          "query",
          "data",
          "warnings",
          "links",
          "meta",
          "attribution",
          "docs"
        ],
        "properties": {
          "endpoint": {
            "type": "string",
            "description": "The endpoint path that produced this response.",
            "example": "/v2/sun"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time",
            "description": "A revision stamp, never a wall clock: identical requests return identical bytes, so a fixed_instant or reference response can be cached for a year and 304 stays reachable.",
            "example": "2026-08-01T00:00:00.000Z"
          },
          "query": {
            "type": "object",
            "description": "The request as understood: every resolved input (instant, location, presentation options) plus `ignored`, in the order spec-00 13.2 defines. Shape is endpoint-specific.",
            "additionalProperties": true
          },
          "data": {
            "description": "The answer. Shape is endpoint-specific; see each operation's summary and /api/reference.html."
          },
          "warnings": {
            "type": "array",
            "description": "Machine-readable notices about this response. Empty when none apply; never changes the HTTP status.",
            "items": {
              "$ref": "#/components/schemas/Warning"
            }
          },
          "links": {
            "type": "object",
            "description": "Related URLs. self reproduces this exact response; docs is always present; others (page, spec, next, first, explain) appear where relevant.",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            },
            "example": {
              "self": "https://www.cyclecalcs.com/v2/sun?at=2026-06-21&lat=51.4778&lon=-0.0015",
              "docs": "https://www.cyclecalcs.com/api/reference.html#sun",
              "spec": "https://www.cyclecalcs.com/v2/openapi.json",
              "page": "https://www.cyclecalcs.com/sunrise-sunset-calculator.html",
              "explain": "https://www.cyclecalcs.com/learn/seasons.html"
            }
          },
          "meta": {
            "type": "object",
            "description": "Provenance: engine version, rights and attribution, cache class, accuracy statement, tzdb version, and endpoint-specific conventions (refraction model, horizon definitions, time scales, and so on). Never subject to `fields` projection.",
            "additionalProperties": true
          },
          "attribution": {
            "type": "string",
            "description": "The credit line, byte-identical to meta.attribution.text.",
            "example": "Computed by CycleCalcs (cyclecalcs.com) with the MIT-licensed Astronomy Engine. Positional astronomy only."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "The human-readable reference section for this endpoint.",
            "example": "https://www.cyclecalcs.com/api/reference.html#sun"
          }
        }
      },
      "Warning": {
        "type": "object",
        "description": "One machine-readable notice. Exactly four keys, in this order (spec-00 11.4). Never changes the HTTP status.",
        "required": [
          "code",
          "message",
          "field",
          "value"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "ambiguous_local_time",
              "assumed_utc",
              "blocked_layer_omitted",
              "dataset_stale",
              "decorative_starfield",
              "dst_discontinuity",
              "duplicate_alarm",
              "duplicate_body",
              "duplicate_depression",
              "duplicate_include",
              "duplicate_pair",
              "duplicate_parameter",
              "duplicate_shower",
              "duplicate_source",
              "elevation_ignored",
              "estimate_only",
              "extrapolated_epoch",
              "field_blocked",
              "fields_ignored",
              "fixed_pair_minima",
              "high_latitude_caution",
              "horizon_overridden",
              "hybrid_eclipse",
              "key_expired",
              "key_expiring",
              "key_invalid",
              "key_revoked",
              "key_transfer_spent",
              "large_intercept",
              "low_altitude_precision",
              "mean_node_model",
              "model_disagreement",
              "model_epoch_only",
              "moon_dominates",
              "no_astronomical_darkness",
              "no_location_orientation",
              "no_search_result",
              "nonexistent_local_time_shifted",
              "not_visible_from_location",
              "oblateness_simplified",
              "orbit_arc_truncated",
              "parameter_ignored",
              "place_ambiguous",
              "place_index_tier_2",
              "place_matched_by_substring",
              "polar_angles_flipped",
              "polar_day",
              "polar_night",
              "polar_region",
              "prose_window_differs",
              "refraction_below_cutoff",
              "render_degenerate",
              "rounded_parameter",
              "shape_ignored",
              "source_window_clipped",
              "station_near_window_edge",
              "station_search_truncated",
              "time_format_not_applicable",
              "time_scale_undefined",
              "timezone_from_place",
              "truncated_results",
              "tz_source_boundary_1970",
              "tzdb_lmt_interval",
              "unknown_parameter",
              "unusual_navigational_body"
            ],
            "description": "A stable, lower_snake_case token. The full registry is at GET /v2/enums?set=warning_codes."
          },
          "message": {
            "type": "string",
            "description": "A human-readable explanation. Not parsed by machines; use `code`."
          },
          "field": {
            "type": [
              "string",
              "null"
            ],
            "description": "The data or query pointer the warning is about, dot-addressed the same way `fields` is. Null when the warning is not about one field."
          },
          "value": {
            "description": "The value involved, when there is a single obvious one to show. Null otherwise."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "An RFC 9457 problem detail document. Served on every non-2xx v2 response with Content-Type application/problem+json.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "parameter",
          "hint",
          "example",
          "supported",
          "request_id",
          "error_chain",
          "docs"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URI identifying the error type, ending in a #fragment matching `code` in kebab-case. Dereferenceable at /api/errors.html.",
            "example": "https://www.cyclecalcs.com/api/errors.html#bad-latitude"
          },
          "title": {
            "type": "string",
            "description": "A short, fixed, human-readable summary of the error type (not the instance).",
            "example": "Latitude is not a number from -90 to 90"
          },
          "status": {
            "type": "integer",
            "enum": [
              400,
              401,
              404,
              405,
              406,
              409,
              410,
              413,
              414,
              429,
              500,
              503
            ],
            "description": "The HTTP status code, repeated from the response line.",
            "example": 400
          },
          "detail": {
            "type": "string",
            "description": "A human-readable explanation specific to this occurrence.",
            "example": "lat must be a number from -90 to 90; got \"999\"."
          },
          "instance": {
            "type": "string",
            "format": "uri",
            "description": "Present only on non-cacheable statuses (429, 503, and unexpected 500s): `type` with ?request_id=<id> inserted before its #fragment.",
            "example": "https://www.cyclecalcs.com/api/errors.html?request_id=502bc1523e53167f#rate-limited"
          },
          "code": {
            "type": "string",
            "enum": [
              "AMBIGUOUS_LOCAL_TIME",
              "AMBIGUOUS_LONGITUDE_FORMAT",
              "AMBIGUOUS_PLACE",
              "BAD_ALARM",
              "BAD_APERTURE",
              "BAD_BOOLEAN",
              "BAD_BORTLE",
              "BAD_CALENDAR_SOURCE",
              "BAD_CLIENT",
              "BAD_COUNT",
              "BAD_CURSOR",
              "BAD_CYCLE_KEY",
              "BAD_DATE",
              "BAD_DEPRESSION",
              "BAD_ECLIPSE_DIRECTION",
              "BAD_ECLIPSE_TYPE",
              "BAD_ELEVATION",
              "BAD_FIELDS",
              "BAD_FORMAT",
              "BAD_HEIGHT_OF_EYE",
              "BAD_HEMISPHERE",
              "BAD_INCLUDE",
              "BAD_INDEX_ERROR",
              "BAD_LATITUDE",
              "BAD_LIMIT",
              "BAD_LIMITING_MAGNITUDE",
              "BAD_LONGITUDE",
              "BAD_MOON_ALTITUDE",
              "BAD_MOTION",
              "BAD_NELM",
              "BAD_NODE_MODEL",
              "BAD_PAIR",
              "BAD_PAIRS",
              "BAD_PARAMETER_VALUE",
              "BAD_PHASE_SELECTOR",
              "BAD_PLACE",
              "BAD_RADEC",
              "BAD_RANGE",
              "BAD_RANGE_VALUE",
              "BAD_REFRACTION",
              "BAD_SEARCH_HORIZON",
              "BAD_SEXTANT_ALTITUDE",
              "BAD_SIZE",
              "BAD_STEP",
              "BAD_TIMEZONE",
              "BAD_TIME_FORMAT",
              "BODY_NOT_GEOCENTRIC",
              "BODY_NOT_SUPPORTED_HERE",
              "BODY_REQUIRED",
              "BODY_UNAVAILABLE",
              "BUDGET_EXCEEDED",
              "CLUSTER_PARAMETERS_REQUIRED",
              "COMPUTATION_FAILED",
              "COMPUTE_BUDGET_EXCEEDED",
              "CONFLICTING_PARAMETERS",
              "CONFLICTING_TIME_INPUTS",
              "DATASET_UNAVAILABLE",
              "DATE_OUT_OF_RANGE",
              "ENDPOINT_BLOCKED",
              "ENDPOINT_RETIRED",
              "ENDPOINT_WITHDRAWN",
              "EXPIRED_KEY",
              "FIELD_NOT_COMPUTED",
              "INCOMPLETE_LOCATION",
              "INCOMPLETE_RANGE",
              "INTERNAL_ERROR",
              "INVALID_CHARACTER",
              "INVALID_KEY",
              "LATITUDE_AT_POLE",
              "LATITUDE_TOO_EXTREME",
              "LOCATION_REQUIRED",
              "LONGITUDE_REQUIRED",
              "METHOD_NOT_ALLOWED",
              "NONEXISTENT_LOCAL_TIME",
              "NOT_ACCEPTABLE",
              "OBJECT_NOT_FOUND",
              "OUTSIDE_MODEL_RANGE",
              "PARAMETER_TOO_LONG",
              "PLACE_INDEX_UNAVAILABLE",
              "PLACE_NOT_FOUND",
              "QUERY_TOO_SHORT",
              "RANGE_TOO_LARGE",
              "RATE_LIMITED",
              "RESPONSE_TOO_LARGE",
              "REVOKED_KEY",
              "SERVICE_UNAVAILABLE",
              "SKY_CLASS_REQUIRED",
              "STAR_POOL_TIMEOUT",
              "STAR_SLOT_FAILED",
              "TIMEZONE_AUTO_UNSUPPORTED",
              "TIMEZONE_DATABASE_UNAVAILABLE",
              "TIMEZONE_REQUIRED",
              "TIME_REQUIRED",
              "TOO_MANY_BODIES",
              "TOO_MANY_CALENDAR_SOURCES",
              "TOO_MANY_DEPRESSIONS",
              "TOO_MANY_PAIRS",
              "TOO_MANY_PARAMETERS",
              "TOO_MANY_SHOWERS",
              "UNKNOWN_BODY",
              "UNKNOWN_ENDPOINT",
              "UNKNOWN_SHOWER",
              "URI_TOO_LONG"
            ],
            "description": "The stable, SCREAMING_SNAKE_CASE error code a client should branch on. The full registry is at GET /v2/enums?set=error_codes.",
            "example": "BAD_LATITUDE"
          },
          "parameter": {
            "type": [
              "string",
              "null"
            ],
            "description": "The query parameter most responsible for this error, when there is one.",
            "example": "lat"
          },
          "hint": {
            "type": [
              "string",
              "null"
            ],
            "description": "A short suggestion for fixing the request.",
            "example": "Use a signed decimal, for example lat=51.5 for north and lat=-51.5 for south. Do not append N or S"
          },
          "example": {
            "type": [
              "string",
              "null"
            ],
            "description": "A worked example URL or value, when one is more useful than the hint alone."
          },
          "supported": {
            "type": [
              "array",
              "null"
            ],
            "items": {},
            "description": "The accepted values for the offending parameter, when the error is about picking from a closed set."
          },
          "request_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "16 lowercase hex characters, present only on non-cacheable statuses. Quote it when reporting a 500.",
            "example": "502bc1523e53167f"
          },
          "error_chain": {
            "type": [
              "array",
              "null"
            ],
            "items": {},
            "description": "For an error caused by resolving another parameter first (rare), the sequence of codes that led here."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "The API reference index.",
            "example": "https://www.cyclecalcs.com/api/reference.html"
          },
          "warnings": {
            "type": "array",
            "description": "Present only on a refusal to a request whose API key was not honored: the key_* warnings a 200 would carry, so a drop to the anonymous tier is never silent. Always the last member. Absent otherwise.",
            "items": {
              "$ref": "#/components/schemas/Warning"
            }
          }
        },
        "additionalProperties": true
      }
    },
    "securitySchemes": {
      "RapidAPIKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-RapidAPI-Key",
        "description": "Optional, and read only by the RapidAPI gateway host (cyclecalcs-astronomy-api3.p.rapidapi.com), never by the origin in `servers`. Calling this API directly needs no key and is served at the free anonymous tier: 300 requests a minute, 2,000 an hour and 5,000 a day, counted per caller rather than globally. A key raises those limits to the subscribed tier and is what monthly volume is metered against; it is sold at https://rapidapi.com/info-8KZIhinZ9/api/cyclecalcs-astronomy-api3. It is never accepted as a query parameter, on either host."
      }
    }
  }
}
