openapi: 3.0.3
info:
  title: Korea Calendar API
  description: >
    Korean public holidays, lunar↔solar date conversion, sexagenary cycle
    (간지/gapja), the 24 solar terms (절기) with astronomically-computed entry
    times, and Korean Revised Romanization (RR) — one endpoint set for Korean
    calendar, fortune (사주/saju) and localization apps. KASI-derived data,
    fully self-contained (no external API at runtime), no API key on the origin.
    Holiday dates are statutory Korean public holidays; lunar & gapja cover
    1391–2050; 24 solar terms are computed astronomically (Meeus algorithm)
    for any year in range with exact dates.
  version: 1.2.0
  contact:
    name: KunStudio
    url: https://korea-calendar-api.kunstudio.workers.dev
  license:
    name: Data — KASI 특일정보 / KOGL Type 1 (commercial use OK, attribution required)
    url: https://www.kogl.or.kr/info/license.do
servers:
  - url: https://api.apilayer.com/korea_calendar
    description: APILayer marketplace gateway (production; APILayer injects the apikey header)
  - url: https://korea-calendar-api.kunstudio.workers.dev
    description: Origin (Cloudflare Worker — direct, no auth; used for evaluation)
security:
  - apikeyAuth: []
tags:
  - name: Holidays
    description: Korean public holidays (incl. 대체공휴일 substitute holidays)
  - name: Lunar & Gapja
    description: Lunar↔solar conversion and 간지 (sexagenary cycle) pillars
  - name: Solar Terms
    description: The 24 solar terms (절기) with KST entry times
  - name: Romanization
    description: Korean Revised Romanization (RR)
  - name: System
    description: Liveness and service metadata
paths:
  /v1/holidays:
    get:
      tags: [Holidays]
      operationId: getHolidays
      summary: Korean public holidays for a year
      description: >
        Returns every Korean public holiday for the given year, including
        대체공휴일 (substitute holidays). Bundled & verified for 2026–2027;
        production syncs KASI (특일정보) for any year.
      parameters:
        - name: year
          in: query
          required: true
          description: 4-digit year (e.g. 2026)
          schema:
            type: integer
            example: 2026
      responses:
        '200':
          description: List of holidays for the year
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HolidaysResponse'
              example:
                year: 2026
                count: 19
                holidays:
                  - date: "2026-01-01"
                    name: "신정"
                    name_en: "New Year's Day"
                    substitute: false
                  - date: "2026-02-17"
                    name: "설날"
                    name_en: "Seollal (Lunar New Year)"
                    substitute: false
                  - date: "2026-03-02"
                    name: "대체공휴일"
                    name_en: "Substitute Holiday"
                    substitute: true
        '400':
          description: Missing year query
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "year query required, e.g. ?year=2026" }
        '404':
          description: Year not in bundled table
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "year 2099 not in bundled table (have 2026-2027). Production syncs KASI for any year." }
  /v1/holidays/is:
    get:
      tags: [Holidays]
      operationId: isHoliday
      summary: Is a given date a public holiday?
      description: Checks whether a specific date is a Korean public holiday and returns its weekday.
      parameters:
        - name: date
          in: query
          required: true
          description: Date as YYYY-MM-DD
          schema:
            type: string
            format: date
            example: "2026-02-17"
      responses:
        '200':
          description: Holiday check result
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IsHolidayResponse' }
              example:
                date: "2026-02-17"
                is_holiday: true
                holiday:
                  date: "2026-02-17"
                  name: "설날"
                  name_en: "Seollal (Lunar New Year)"
                  substitute: false
                weekday: "Tue"
        '400':
          description: Missing or malformed date
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "date query required as YYYY-MM-DD" }
        '404':
          description: Year of the date not in bundled table
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/today:
    get:
      tags: [Holidays]
      operationId: getToday
      summary: Today's Korean date (KST) + holiday status
      description: Returns today's date in Korea Standard Time (UTC+9) and whether it is a public holiday.
      responses:
        '200':
          description: Today in KST
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TodayResponse' }
              example:
                date_kst: "2026-06-14"
                is_holiday: false
                holiday: null
  /v1/romanize:
    get:
      tags: [Romanization]
      operationId: romanize
      summary: Revised Romanization (RR) of Korean text
      description: >
        Converts Hangul to the official Revised Romanization of Korean (국립국어원
        RR table). Per-syllable mapping; full inter-syllable assimilation is
        partial in v1.
      parameters:
        - name: text
          in: query
          required: true
          description: Korean (Hangul) text to romanize
          schema:
            type: string
            example: "안녕하세요"
      responses:
        '200':
          description: Romanized text
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RomanizeResponse' }
              example:
                input: "안녕하세요"
                romanized: "annyeonghaseyo"
                system: "Revised Romanization (RR). Per-syllable mapping; full inter-syllable assimilation is partial in v1."
        '400':
          description: Missing text query
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "text query required, e.g. ?text=안녕하세요" }
  /v1/lunar/solar-to-lunar:
    get:
      tags: [Lunar & Gapja]
      operationId: solarToLunar
      summary: Convert a solar (Gregorian) date to lunar + 간지
      description: >
        Converts a solar date to the Korean lunar calendar and returns the
        간지 (sexagenary cycle) year/month/day pillars. KASI-derived, covers
        1391–2050.
      parameters:
        - name: date
          in: query
          required: true
          description: Solar date as YYYY-MM-DD
          schema:
            type: string
            format: date
            example: "2026-02-17"
      responses:
        '200':
          description: Lunar date + gapja
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SolarToLunarResponse' }
              example:
                solar: "2026-02-17"
                lunar: { year: 2026, month: 1, day: 1, leap_month: false }
                gapja: { year: "병오년", month: "경인월", day: "임술일" }
        '400':
          description: Malformed date or out of supported range
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "date out of supported range (1391-2050)" }
  /v1/lunar/lunar-to-solar:
    get:
      tags: [Lunar & Gapja]
      operationId: lunarToSolar
      summary: Convert a lunar date to solar (Gregorian)
      description: Converts a Korean lunar date to its solar date. Set leap=true for a leap month (윤달).
      parameters:
        - name: date
          in: query
          required: true
          description: Lunar date as YYYY-MM-DD
          schema:
            type: string
            format: date
            example: "2026-01-01"
        - name: leap
          in: query
          required: false
          description: Whether the lunar month is a leap month (윤달)
          schema:
            type: boolean
            default: false
            example: false
      responses:
        '200':
          description: Solar date
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LunarToSolarResponse' }
              example:
                lunar: { year: 2026, month: 1, day: 1, leap_month: false }
                solar: "2026-02-17"
        '400':
          description: Malformed/invalid lunar date or out of supported range
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "lunar date out of supported range (1391-2050) or invalid" }
  /v1/gapja:
    get:
      tags: [Lunar & Gapja]
      operationId: getGapja
      summary: 간지 (sexagenary cycle) pillars for a solar date
      description: >
        Returns the 간지 (육십갑자 / sexagenary cycle) year, month and day pillars
        for a solar date, in both Hangul (e.g. 병오년) and Hanja (e.g. 丙午年),
        plus the lunar date. Core input for 사주/saju and fortune apps.
      parameters:
        - name: date
          in: query
          required: true
          description: Solar date as YYYY-MM-DD
          schema:
            type: string
            format: date
            example: "2026-02-17"
      responses:
        '200':
          description: Gapja pillars
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GapjaResponse' }
              example:
                solar: "2026-02-17"
                gapja: { year: "병오년", month: "경인월", day: "임술일" }
                gapja_chinese: { year: "丙午年", month: "庚寅月", day: "壬戌日" }
                lunar: { year: 2026, month: 1, day: 1, leap_month: false }
                note: "간지 (육십갑자/sexagenary cycle). Year pillar changes at 입춘 in some saju systems; this uses the lunar-calendar convention of korean-lunar-calendar."
        '400':
          description: Malformed date or out of supported range
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "date out of supported range (1391-2050)" }
  /v1/solar-terms:
    get:
      tags: [Solar Terms]
      operationId: getSolarTerms
      summary: The 24 solar terms (절기) for a year, or check a date
      description: >
        Provide ?year to get all 24 solar terms (절기) for that year with KST
        entry times (절입시간), or provide ?date to check whether a specific KST
        date is a solar term. Computed astronomically (Meeus algorithm — no
        lookup table) for any year 1391–2050. Dates are exact; entry-time
        minutes are within ~±15 min of KASI's full-VSOP87.
      parameters:
        - name: year
          in: query
          required: false
          description: 4-digit year (use either year or date)
          schema:
            type: integer
            example: 2026
        - name: date
          in: query
          required: false
          description: KST date as YYYY-MM-DD (use either year or date)
          schema:
            type: string
            format: date
            example: "2026-03-20"
      responses:
        '200':
          description: Either all terms for the year, or a single-date check
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SolarTermsYearResponse'
                  - $ref: '#/components/schemas/SolarTermDateResponse'
              examples:
                year:
                  summary: All 24 terms for a year (?year=2026)
                  value:
                    year: 2026
                    count: 24
                    solar_terms:
                      - date: "2026-01-05"
                        time_kst: "17:17"
                        datetime_kst: "2026-01-05T17:17:47+09:00"
                        name: "소한"
                        romanized: "Sohan"
                        name_en: "Minor Cold"
                        sun_longitude: 285
                date:
                  summary: Single-date check (?date=2026-03-20)
                  value:
                    date: "2026-03-20"
                    is_solar_term: true
                    solar_term:
                      date: "2026-03-20"
                      time_kst: "23:36"
                      datetime_kst: "2026-03-20T23:36:29+09:00"
                      name: "춘분"
                      romanized: "Chunbun"
                      name_en: "Spring Equinox"
                      sun_longitude: 0
        '400':
          description: Missing/invalid year or date, or out of supported range
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: "year (or date) query required, e.g. ?year=2026 or ?date=2026-03-20" }
  /v1/health:
    get:
      tags: [System]
      operationId: health
      summary: Liveness check
      responses:
        '200':
          description: Service is live
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
components:
  securitySchemes:
    apikeyAuth:
      type: apiKey
      in: header
      name: apikey
      description: APILayer API key (injected by the APILayer gateway; the origin requires no key).
  schemas:
    Holiday:
      type: object
      properties:
        date: { type: string, format: date, example: "2026-02-17" }
        name: { type: string, description: Korean name, example: "설날" }
        name_en: { type: string, description: English name, example: "Seollal (Lunar New Year)" }
        substitute: { type: boolean, description: True if 대체공휴일 (substitute holiday), example: false }
    HolidaysResponse:
      type: object
      properties:
        year: { type: integer, example: 2026 }
        count: { type: integer, example: 19 }
        holidays:
          type: array
          items: { $ref: '#/components/schemas/Holiday' }
    IsHolidayResponse:
      type: object
      properties:
        date: { type: string, format: date }
        is_holiday: { type: boolean }
        holiday:
          oneOf:
            - $ref: '#/components/schemas/Holiday'
            - type: 'null'
        weekday: { type: string, example: "Tue", enum: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] }
    TodayResponse:
      type: object
      properties:
        date_kst: { type: string, format: date }
        is_holiday: { type: boolean }
        holiday:
          oneOf:
            - $ref: '#/components/schemas/Holiday'
            - type: 'null'
    RomanizeResponse:
      type: object
      properties:
        input: { type: string }
        romanized: { type: string }
        system: { type: string }
    LunarDate:
      type: object
      properties:
        year: { type: integer }
        month: { type: integer }
        day: { type: integer }
        leap_month: { type: boolean }
    GapjaPillars:
      type: object
      properties:
        year: { type: string, example: "병오년" }
        month: { type: string, example: "경인월" }
        day: { type: string, example: "임술일" }
    SolarToLunarResponse:
      type: object
      properties:
        solar: { type: string, format: date }
        lunar: { $ref: '#/components/schemas/LunarDate' }
        gapja: { $ref: '#/components/schemas/GapjaPillars' }
    LunarToSolarResponse:
      type: object
      properties:
        lunar: { $ref: '#/components/schemas/LunarDate' }
        solar: { type: string, format: date }
    GapjaResponse:
      type: object
      properties:
        solar: { type: string, format: date }
        gapja: { $ref: '#/components/schemas/GapjaPillars' }
        gapja_chinese: { $ref: '#/components/schemas/GapjaPillars' }
        lunar: { $ref: '#/components/schemas/LunarDate' }
        note: { type: string }
    SolarTerm:
      type: object
      properties:
        date: { type: string, format: date }
        time_kst: { type: string, example: "17:17" }
        datetime_kst: { type: string, example: "2026-01-05T17:17:47+09:00" }
        name: { type: string, example: "소한" }
        romanized: { type: string, example: "Sohan" }
        name_en: { type: string, example: "Minor Cold" }
        sun_longitude: { type: integer, example: 285 }
    SolarTermsYearResponse:
      type: object
      properties:
        year: { type: integer }
        count: { type: integer, example: 24 }
        solar_terms:
          type: array
          items: { $ref: '#/components/schemas/SolarTerm' }
    SolarTermDateResponse:
      type: object
      properties:
        date: { type: string, format: date }
        is_solar_term: { type: boolean }
        solar_term:
          oneOf:
            - $ref: '#/components/schemas/SolarTerm'
            - type: 'null'
    Error:
      type: object
      properties:
        error: { type: string }
