openapi: "3.1.0"
info:
  title: JDay API
  version: "1.0.0"
  description: |
    Business-day computation API for AI agents.

    JDay provides correct business-day math across 20 countries,
    handling weekends, public holidays, observed dates, and regional calendars.

    **Base URL:** `https://jday-api.jday.workers.dev`

    **Supported date range:** 2025-01-01 through 2027-12-31 inclusive.

    **Authentication:** All endpoints require `Authorization: Bearer <api_key>`.
  contact:
    name: JDay Support
    url: https://jday.j-stack.net
  license:
    name: Proprietary

servers:
  - url: https://jday-api.jday.workers.dev
    description: Production

security:
  - bearerAuth: []

paths:
  /v1/is-business-day:
    get:
      operationId: isBusinessDay
      summary: Check if a date is a business day
      description: |
        Returns whether the given date is a business day for the specified country
        and optional region. Includes the reason, any holidays on that date,
        and the nearest next/previous business days.
      parameters:
        - name: date
          in: query
          required: true
          description: The date to check in YYYY-MM-DD format. Must be within the supported range (2025-01-01 to 2027-12-31).
          schema:
            type: string
            format: date
            example: "2026-04-03"
        - name: country
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code. Case-insensitive.
          schema:
            type: string
            example: "ZA"
        - name: region
          in: query
          required: false
          description: ISO 3166-2 subdivision suffix only (e.g. `NY`, not `US-NY`). Requires a paid plan.
          schema:
            type: string
            example: "NY"
      responses:
        "200":
          description: Business day check result.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IsBusinessDayResponse"
              example:
                date: "2026-04-03"
                country: "ZA"
                region: null
                is_business_day: false
                reason: "public_holiday"
                holidays:
                  - date: "2026-04-03"
                    official_date: null
                    name: "Good Friday"
                    local_name: null
                    type: "public"
                    date_accuracy: "exact"
                timezone: "Africa/Johannesburg"
                next_business_day: "2026-04-07"
                previous_business_day: "2026-04-02"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanUpgradeRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/add-business-days:
    get:
      operationId: addBusinessDays
      summary: Add or subtract business days from a date
      description: |
        Computes the resulting date after adding (or subtracting) the specified
        number of business days. The start date is never counted. Returns metadata
        about skipped weekends and holidays.

        **Counting rule:** The start date is excluded. Computation moves one calendar
        day at a time in the sign direction of `days`. Only business days count toward
        the target.

        **`days = 0`** returns the input date unchanged with zero skipped metadata.
      parameters:
        - name: date
          in: query
          required: true
          description: The starting date in YYYY-MM-DD format. Must be within the supported range.
          schema:
            type: string
            format: date
            example: "2026-04-01"
        - name: country
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code. Case-insensitive.
          schema:
            type: string
            example: "DE"
        - name: days
          in: query
          required: true
          description: |
            Number of business days to add. Positive = forward, negative = backward, zero = no-op.
            Must be a base-10 integer within signed 32-bit range. No decimals or scientific notation.
          schema:
            type: integer
            format: int32
            example: 10
        - name: region
          in: query
          required: false
          description: ISO 3166-2 subdivision suffix only. Requires a paid plan.
          schema:
            type: string
      responses:
        "200":
          description: Business day addition result.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddBusinessDaysResponse"
              example:
                start_date: "2026-04-01"
                business_days_added: 10
                end_date: "2026-04-16"
                country: "DE"
                region: null
                holidays_skipped:
                  - date: "2026-04-03"
                    official_date: null
                    name: "Good Friday"
                    local_name: null
                    type: "public"
                    date_accuracy: "exact"
                  - date: "2026-04-06"
                    official_date: null
                    name: "Easter Monday"
                    local_name: null
                    type: "public"
                    date_accuracy: "exact"
                weekends_skipped: 4
                calendar_days_elapsed: 15
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanUpgradeRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/count-business-days:
    get:
      operationId: countBusinessDays
      summary: Count business days between two dates
      description: |
        Counts the number of business days between `start` and `end`.

        **Counting rule:** The start date is excluded. The end date is included.
        If `start` equals `end`, all counts are zero. If `start > end`, `business_days`
        is negative while absolute counts remain non-negative.
      parameters:
        - name: start
          in: query
          required: true
          description: Start date in YYYY-MM-DD format. This date is excluded from the count.
          schema:
            type: string
            format: date
            example: "2026-01-01"
        - name: end
          in: query
          required: true
          description: End date in YYYY-MM-DD format. This date is included in the count.
          schema:
            type: string
            format: date
            example: "2026-03-31"
        - name: country
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code. Case-insensitive.
          schema:
            type: string
            example: "US"
        - name: region
          in: query
          required: false
          description: ISO 3166-2 subdivision suffix only. Requires a paid plan.
          schema:
            type: string
      responses:
        "200":
          description: Business day count result.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CountBusinessDaysResponse"
              example:
                start_date: "2026-01-01"
                end_date: "2026-03-31"
                business_days: 62
                total_calendar_days: 89
                weekends: 26
                holidays_on_weekdays: 1
                holidays:
                  - date: "2026-01-19"
                    official_date: null
                    name: "Martin Luther King Jr. Day"
                    local_name: null
                    type: "public"
                    date_accuracy: "exact"
                country: "US"
                region: null
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanUpgradeRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/holidays:
    get:
      operationId: getHolidays
      summary: List holidays for a country and year
      description: |
        Returns all holidays for the specified country and year.
        Regional holidays are only included when a valid `region` parameter is supplied.
        The `year` parameter is required in the REST API.
      parameters:
        - name: country
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code. Case-insensitive.
          schema:
            type: string
            example: "JP"
        - name: year
          in: query
          required: true
          description: Year to retrieve holidays for. Must be 2025, 2026, or 2027.
          schema:
            type: integer
            enum: [2025, 2026, 2027]
            example: 2026
        - name: region
          in: query
          required: false
          description: ISO 3166-2 subdivision suffix only. Requires a paid plan.
          schema:
            type: string
      responses:
        "200":
          description: List of holidays.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HolidaysResponse"
              example:
                country: "JP"
                year: 2026
                region: null
                holidays:
                  - date: "2026-01-01"
                    official_date: null
                    name: "New Year's Day"
                    local_name: "元日"
                    type: "public"
                    date_accuracy: "exact"
                  - date: "2026-01-12"
                    official_date: null
                    name: "Coming of Age Day"
                    local_name: "成人の日"
                    type: "public"
                    date_accuracy: "exact"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanUpgradeRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/countries:
    get:
      operationId: listCountries
      summary: List supported countries and metadata
      description: |
        Returns all supported countries with weekend rules, timezone hints,
        region support status, and available regions.
      responses:
        "200":
          description: List of supported countries.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CountriesResponse"
              example:
                countries:
                  - code: "US"
                    name: "United States"
                    weekend_days: [6, 7]
                    timezone: "America/New_York"
                    region_support: true
                    regions:
                      - code: "CA"
                        name: "California"
                      - code: "NY"
                        name: "New York"
                    notes: []
                  - code: "ZA"
                    name: "South Africa"
                    weekend_days: [6, 7]
                    timezone: "Africa/Johannesburg"
                    region_support: false
                    regions: []
                    notes: []
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key passed as `Authorization: Bearer <api_key>`"

  headers:
    X-Request-Id:
      description: Server-generated UUIDv7 request identifier. Present on every response.
      schema:
        type: string
        format: uuid
        example: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"

  schemas:
    HolidayRef:
      type: object
      required: [date, official_date, name, local_name, type, date_accuracy]
      properties:
        date:
          type: string
          format: date
          description: The effective non-working date used by the engine for business-day computation.
        official_date:
          type: ["string", "null"]
          format: date
          description: The official/legal holiday date when it differs from `date`, otherwise `null`.
        name:
          type: string
          description: English display name of the holiday.
        local_name:
          type: ["string", "null"]
          description: Localized/native name when available, otherwise `null`.
        type:
          type: string
          enum: [public, regional]
          description: Whether this is a national public holiday or a regional holiday.
        date_accuracy:
          type: string
          enum: [exact, estimated]
          description: Whether the date is fixed/rule-based (`exact`) or may shift due to late confirmation (`estimated`).

    ErrorResponse:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code.
              enum:
                - missing_parameter
                - invalid_date
                - invalid_year
                - invalid_days
                - invalid_country
                - invalid_region
                - date_out_of_range
                - unauthorized
                - plan_upgrade_required
                - method_not_allowed
                - rate_limited
                - monthly_limit_reached
                - internal_error
            message:
              type: string
              description: Human-readable error message.
        request_id:
          type: string
          format: uuid
          description: Server-generated UUIDv7 for this request.

    IsBusinessDayResponse:
      type: object
      required: [date, country, region, is_business_day, reason, holidays, timezone, next_business_day, previous_business_day]
      properties:
        date:
          type: string
          format: date
        country:
          type: string
        region:
          type: ["string", "null"]
        is_business_day:
          type: boolean
        reason:
          type: string
          enum: [business_day, weekend, public_holiday, regional_holiday]
          description: |
            Primary classification. Precedence: weekend > regional_holiday > public_holiday > business_day.
        holidays:
          type: array
          items:
            $ref: "#/components/schemas/HolidayRef"
          description: Holiday records for the queried date. Empty array if no holidays.
        timezone:
          type: string
          description: Primary timezone hint for the country. Metadata only.
        next_business_day:
          type: ["string", "null"]
          format: date
          description: Next business day within the supported range, or `null` at the range boundary.
        previous_business_day:
          type: ["string", "null"]
          format: date
          description: Previous business day within the supported range, or `null` at the range boundary.

    AddBusinessDaysResponse:
      type: object
      required: [start_date, business_days_added, end_date, country, region, holidays_skipped, weekends_skipped, calendar_days_elapsed]
      properties:
        start_date:
          type: string
          format: date
        business_days_added:
          type: integer
          description: Echoes the signed input value.
        end_date:
          type: string
          format: date
        country:
          type: string
        region:
          type: ["string", "null"]
        holidays_skipped:
          type: array
          items:
            $ref: "#/components/schemas/HolidayRef"
          description: |
            Holiday records on traversed non-weekend dates, sorted chronologically
            regardless of traversal direction.
        weekends_skipped:
          type: integer
          minimum: 0
          description: Absolute count of traversed weekend dates.
        calendar_days_elapsed:
          type: integer
          description: Signed count of calendar days. Positive forward, negative backward, zero for days=0.

    CountBusinessDaysResponse:
      type: object
      required: [start_date, end_date, business_days, total_calendar_days, weekends, holidays_on_weekdays, holidays, country, region]
      properties:
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
        business_days:
          type: integer
          description: May be negative when start > end.
        total_calendar_days:
          type: integer
          minimum: 0
          description: Absolute count of calendar days in the traversed interval.
        weekends:
          type: integer
          minimum: 0
          description: Absolute count of weekend dates in the traversed interval.
        holidays_on_weekdays:
          type: integer
          minimum: 0
          description: Count of distinct traversed dates that are weekday holidays.
        holidays:
          type: array
          items:
            $ref: "#/components/schemas/HolidayRef"
          description: |
            All holiday records on weekday-holiday dates in the interval.
            Excludes holidays falling on weekends. Sorted chronologically then by name.
        country:
          type: string
        region:
          type: ["string", "null"]

    HolidaysResponse:
      type: object
      required: [country, year, region, holidays]
      properties:
        country:
          type: string
        year:
          type: integer
        region:
          type: ["string", "null"]
        holidays:
          type: array
          items:
            $ref: "#/components/schemas/HolidayRef"
          description: Sorted by date ascending, then name ascending.

    CountriesResponse:
      type: object
      required: [countries]
      properties:
        countries:
          type: array
          items:
            $ref: "#/components/schemas/Country"
          description: Sorted by code ascending.

    Country:
      type: object
      required: [code, name, weekend_days, timezone, region_support, regions, notes]
      properties:
        code:
          type: string
          description: ISO 3166-1 alpha-2 country code.
        name:
          type: string
        weekend_days:
          type: array
          items:
            type: integer
          description: "Weekend day numbers. 1=Mon through 7=Sun."
        timezone:
          type: string
          description: Primary timezone hint. Metadata only.
        region_support:
          type: boolean
        regions:
          type: array
          items:
            type: object
            required: [code, name]
            properties:
              code:
                type: string
                description: ISO 3166-2 subdivision suffix.
              name:
                type: string
          description: Empty array for countries without region support.
        notes:
          type: array
          items:
            type: string
          description: Always present. Empty array when no notes.

  responses:
    BadRequest:
      description: Invalid request parameters.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            missing_parameter:
              summary: Required parameter missing
              value:
                error:
                  code: missing_parameter
                  message: "Required query parameter 'date' is missing."
                request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"
            invalid_country:
              summary: Unsupported country code
              value:
                error:
                  code: invalid_country
                  message: "Country code must be one of the supported ISO 3166-1 alpha-2 values."
                request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"
            date_out_of_range:
              summary: Date outside supported range
              value:
                error:
                  code: date_out_of_range
                  message: "Date must be between 2025-01-01 and 2027-12-31 inclusive."
                request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"

    Unauthorized:
      description: Missing, malformed, or invalid API credentials.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
        WWW-Authenticate:
          schema:
            type: string
            example: 'Bearer realm="JDay"'
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: unauthorized
              message: "Valid API authentication is required."
            request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"

    PlanUpgradeRequired:
      description: The request requires a higher plan.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: plan_upgrade_required
              message: "Regional queries require a Starter plan or above."
            request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"

    RateLimited:
      description: Rate limit exceeded.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
        RateLimit-Limit:
          schema:
            type: integer
        RateLimit-Remaining:
          schema:
            type: integer
        RateLimit-Reset:
          schema:
            type: integer
            description: Delta-seconds until at least one token is available.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: rate_limited
              message: "Per-key rate limit exceeded. Try again shortly."
            request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"

    InternalError:
      description: Internal server error.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: internal_error
              message: "An internal error occurred. Please try again later."
            request_id: "018f7d1a-3a51-7f4b-9c78-830f8b6ce220"
