{
  "openapi": "3.0.0",
  "info": {
    "title": "Sunrise Sunset API",
    "description": "API for retrieving sunrise, sunset, twilight, golden/blue hour, solar position and moon data for a given location and date.\nAll example values below are a real response for lat=38.99 lng=-77.03 (Washington DC) on 2026-07-26 at elevation 0, in the default 12-hour format.",
    "version": "1.0.0",
    "contact": {
      "url": "https://sunrisesunset.io/api/"
    }
  },
  "servers": [
    {
      "url": "https://api.sunrisesunset.io",
      "description": "Production server"
    }
  ],
  "paths": {
    "/json": {
      "get": {
        "summary": "Get sun times for a location",
        "description": "Returns sunrise, sunset, and other solar event times for given coordinates and date(s). Responses are CORS-enabled (Access-Control- Allow-Origin: *) and cached: a request for an explicit date is cached for 7 days, a request with no date for 1 hour.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Latitude coordinate, -90 to 90. Characters outside [0-9.-] are stripped before parsing, so \"38.99N\" is read as 38.99 (the N is discarded, not interpreted as a hemisphere).",
            "example": "38.99"
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Longitude coordinate, -180 to 180. Stripped the same way as lat, so \"77.03W\" reads as +77.03 and NOT as -77.03.",
            "example": "-77.03"
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "IANA timezone for the results. Auto-detected from the coordinates when omitted. A value starting with \"gmt\" is rewritten to \"Etc/<value>\". An unrecognised value does not fail the request: the response still returns 200 with the auto-detected zone, status INVALID_TZID and an `error` message.",
            "example": "America/New_York"
          },
          {
            "name": "date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Single date. Parsed leniently: \"2026-07-26\", \"07/26/2026\", \"26 July 2026\", \"20260726\" and relative words like \"today\", \"tomorrow\" and \"yesterday\" all work. Defaults to the current date.",
            "example": "2026-07-26"
          },
          {
            "name": "date_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Start date for a range; must be used with date_end. The range cannot exceed 1 year, and `results` becomes an array.",
            "example": "2026-07-26"
          },
          {
            "name": "date_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "End date for a range; must be used with date_start.",
            "example": "2026-07-28"
          },
          {
            "name": "time_format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Format for every time value in the response. Four values are recognised: \"12\" (h:mm:ss A, the default), \"24\" (HH:mm:ss), \"military\" (HHmm) and \"unix\" (epoch seconds as a STRING, which also forces results.timezone to \"UTC\" and results.utc_offset to 0 while leaving the top-level tzid at the resolved zone).\nAny OTHER value is not rejected or ignored — it is passed through to dayjs as a raw format string, so time_format=YYYY returns \"2026\" and time_format=bogus returns \"bogu45\". Deliberately not an enum, because the API does accept arbitrary values here.",
            "example": "12"
          },
          {
            "name": "formatted",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Set to 0 for ISO 8601 output (\"2026-07-26T06:03:45-04:00\") and a day_length in whole seconds. Any other value, including absent, gives the 12-hour clock and an H:mm:ss day_length. When both are supplied, time_format wins for the time fields while formatted still governs day_length.",
            "example": "0"
          },
          {
            "name": "elevation",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Observer elevation in metres above sea level, which shifts every rise/set and twilight time. Pass \"false\" or \"0\" to skip it, a number to force that value (negatives clamp to 0, unparseable text reads as 0), or omit it to auto-detect the terrain elevation for the coordinates. If auto-detection fails the request still succeeds with elevation 0.",
            "example": "1609"
          },
          {
            "name": "callback",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z_$][a-zA-Z0-9_$]*$"
            },
            "description": "JSONP callback name. When present and a valid JavaScript identifier, the body is wrapped as `name(...)` and served as application/javascript. A name failing the pattern is ignored entirely — plain JSON is returned and the value is never reflected into the body or headers.",
            "example": "myCallback"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Also returned when the supplied timezone was invalid, in which case status is INVALID_TZID and an `error` key is appended — the results are complete and use the auto-detected zone.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/SunTimes"
                        },
                        {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SunTimes"
                          }
                        }
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "OK",
                        "INVALID_TZID"
                      ]
                    },
                    "tzid": {
                      "type": "string",
                      "description": "The timezone the times were computed in.",
                      "example": "America/New_York"
                    },
                    "error": {
                      "type": "string",
                      "description": "Present only when status is INVALID_TZID.",
                      "example": "Invalid timezone provided, times are in UTC or auto-detected timezone."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid coordinates or an invalid/unusable date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "description": "The error envelope. Note it carries no `tzid`, and the message is under `error` — not `body`.",
        "properties": {
          "results": {
            "type": "object",
            "nullable": true,
            "description": "Always null on an error.",
            "example": null
          },
          "status": {
            "type": "string",
            "enum": [
              "INVALID_REQUEST",
              "INVALID_DATE",
              "UNKNOWN_ERROR"
            ],
            "example": "INVALID_REQUEST"
          },
          "error": {
            "type": "string",
            "example": "Latitude must be between -90 and 90 degrees."
          }
        }
      },
      "TimeWindow": {
        "type": "object",
        "description": "A begin/end pair in the same time format as every other time field. Each half is independently nullable — at high latitudes the sun can cross one boundary angle on a given day without crossing the other.",
        "properties": {
          "begin": {
            "type": "string",
            "nullable": true,
            "example": "7:46:57 PM"
          },
          "end": {
            "type": "string",
            "nullable": true,
            "example": "8:43:27 PM"
          }
        }
      },
      "SunTimes": {
        "type": "object",
        "description": "One day of results. Every clock time is rendered in the format selected by `formatted`/`time_format`, and is null when the event does not occur (polar day and polar night, chiefly). Times are local to `timezone`; a sunset after local midnight carries the following calendar date, so in ISO mode the date part can be the next day.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "description": "The date the results are for. Known issue: for zones east of UTC+12:00 this echoes the previous calendar day. The times themselves are correct for the requested day.",
            "example": "2026-07-26"
          },
          "sunrise": {
            "type": "string",
            "nullable": true,
            "description": "Sun's upper edge crosses the horizon, rising (-0.833 degrees).",
            "example": "6:03:45 AM"
          },
          "sunset": {
            "type": "string",
            "nullable": true,
            "description": "Sun's upper edge crosses the horizon, setting (-0.833 degrees).",
            "example": "8:25:07 PM"
          },
          "first_light": {
            "type": "string",
            "nullable": true,
            "description": "Astronomical dawn — the first hint of light (sun at -18 degrees).",
            "example": "4:14:53 AM"
          },
          "last_light": {
            "type": "string",
            "nullable": true,
            "description": "Astronomical dusk — full darkness begins (sun at -18 degrees).",
            "example": "10:13:30 PM"
          },
          "dawn": {
            "type": "string",
            "nullable": true,
            "description": "Civil dawn (sun at -6 degrees).",
            "example": "5:33:29 AM"
          },
          "dusk": {
            "type": "string",
            "nullable": true,
            "description": "Civil dusk (sun at -6 degrees).",
            "example": "8:55:18 PM"
          },
          "solar_noon": {
            "type": "string",
            "nullable": true,
            "description": "Sun's highest point. Present on polar days and polar nights, when sunrise and sunset are both null.",
            "example": "1:14:41 PM"
          },
          "golden_hour": {
            "type": "string",
            "nullable": true,
            "description": "Start of the evening golden hour. Retained unchanged; equal to golden_hour_evening.begin.",
            "example": "7:46:57 PM"
          },
          "day_length": {
            "nullable": true,
            "oneOf": [
              {
                "type": "string",
                "description": "H:mm:ss, the default."
              },
              {
                "type": "number",
                "description": "Whole seconds, when formatted=0."
              }
            ],
            "description": "Time between sunrise and sunset. An H:mm:ss string by default and a number of seconds when formatted=0 — governed by `formatted` alone, never by time_format. Null whenever sunrise/sunset are null, including polar day and polar night.",
            "example": "14:21:21"
          },
          "nautical_twilight_begin": {
            "type": "string",
            "nullable": true,
            "description": "Nautical dawn (sun at -12 degrees).",
            "example": "4:56:06 AM"
          },
          "nautical_twilight_end": {
            "type": "string",
            "nullable": true,
            "description": "Nautical dusk (sun at -12 degrees).",
            "example": "9:32:31 PM"
          },
          "timezone": {
            "type": "string",
            "description": "IANA zone the times are expressed in. Forced to \"UTC\" when time_format=unix.",
            "example": "America/New_York"
          },
          "utc_offset": {
            "type": "integer",
            "description": "Offset from UTC in whole MINUTES (not hours, not seconds), so -240 is UTC-4 and 765 is UTC+12:45. Forced to 0 when time_format=unix.",
            "example": -240
          },
          "sun_altitude": {
            "type": "number",
            "nullable": true,
            "minimum": -90,
            "maximum": 90,
            "description": "Apparent (refraction-corrected) altitude of the sun above the horizon at solar noon, in degrees, rounded to 2dp. Negative during a polar night.",
            "example": 70.33
          },
          "sun_azimuth": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 360,
            "description": "Compass bearing of the sun at solar noon, degrees clockwise from north (90 east, 180 south, 270 west), rounded to 2dp. Wrapped into [0,360), but a bearing just under 360 can round up to exactly 360.",
            "example": 180
          },
          "sunrise_azimuth": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 360,
            "description": "Compass bearing of the sunrise, degrees clockwise from north, 2dp.",
            "example": 63.97
          },
          "sunset_azimuth": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 360,
            "description": "Compass bearing of the sunset, degrees clockwise from north, 2dp.",
            "example": 295.84
          },
          "moonrise": {
            "type": "string",
            "nullable": true,
            "description": "Moonrise falling within the requested LOCAL calendar day. Null on days when the moon does not rise — roughly one day a month, which is ordinary and does not imply moon_always_up/down.",
            "example": "6:50:51 PM"
          },
          "moonset": {
            "type": "string",
            "nullable": true,
            "description": "Moonset falling within the requested local calendar day.",
            "example": "2:54:06 AM"
          },
          "moon_illumination": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Illuminated fraction of the moon's disc as a PERCENTAGE, 2dp, evaluated at the local-noon anchor.",
            "example": 92.05
          },
          "moon_phase": {
            "type": "string",
            "enum": [
              "New Moon",
              "Waxing Crescent",
              "First Quarter",
              "Waxing Gibbous",
              "Full Moon",
              "Waning Gibbous",
              "Last Quarter",
              "Waning Crescent"
            ],
            "description": "Phase name. These are eight equal buckets of the lunation, so \"First Quarter\" names the whole span around that phase, not the instant of it.",
            "example": "Waxing Gibbous"
          },
          "moon_phase_value": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Position through the lunation, 2dp: 0 new, 0.25 first quarter, 0.5 full, 0.75 last quarter, wrapping at 1.",
            "example": 0.41
          },
          "moon_always_up": {
            "type": "boolean",
            "description": "The moon is circumpolar and never sets on this day. Only ever true when both moonrise and moonset are null.",
            "example": false
          },
          "moon_always_down": {
            "type": "boolean",
            "description": "The moon never clears the horizon on this day. Only ever true when both moonrise and moonset are null.",
            "example": false
          },
          "elevation": {
            "type": "number",
            "minimum": 0,
            "description": "Observer elevation in metres actually used for the calculation — the `elevation` parameter when supplied, the auto-detected terrain value otherwise, or 0.",
            "example": 0
          },
          "sun_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "normal",
              "midnight_sun",
              "polar_night"
            ],
            "description": "midnight_sun when the sun never sets that day, polar_night when it never rises; normal otherwise. Both polar cases return null for sunrise, sunset and day_length. Null if the coordinates could not be resolved.",
            "example": "normal"
          },
          "golden_hour_morning": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeWindow"
              }
            ],
            "description": "Sun between -4 and +6 degrees while rising."
          },
          "golden_hour_evening": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeWindow"
              }
            ],
            "description": "Sun between +6 and -4 degrees while setting."
          },
          "blue_hour_morning": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeWindow"
              }
            ],
            "description": "Sun between -6 and -4 degrees while rising; ends where golden_hour_morning begins."
          },
          "blue_hour_evening": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeWindow"
              }
            ],
            "description": "Sun between -4 and -6 degrees while setting; begins where golden_hour_evening ends."
          }
        }
      }
    }
  }
}