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.

    All 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).

            Any 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.
