{
  "openapi": "3.1.0",
  "info": {
    "title": "Kalenteri.online API",
    "version": "1.2.0",
    "description": "Finnish calendar events and moon phases for websites, applications and other services. Event names and display labels are in Finnish.\n\n**Free access**\n\n- Current-year calendar events (`id`, `name`, `publicHoliday` and `flagDay`) and moon phases. `type=modern` includes the full modern calendar view and its selected theme days. `type=flagDays` includes all flag days in the dataset. No registration or API key is required.\n- Personal and commercial use is permitted with attribution: Calendar data: [Kalenteri.online](https://kalenteri.online/), or equivalent wording in the language of your service.\n- You may not resell the data as-is or use it to offer a competing calendar service or calendar API.\n\n**Rate limits and caching**\n\nFree calendar and moon-phase requests share a quota of **100 requests per IP per day** and **10 requests per IP per minute**. The daily quota resets at midnight in Finland (`Europe/Helsinki`).\n\nFetch the full year and cache it locally. Refresh at most once a day and follow the `Cache-Control` and `Expires` headers. Do not call the API on every page load.\n\n**Paid access**\n\nSubscriptions include an extended dataset for the selected years, Outlook export, commercial use without mandatory attribution and API access for 12 months. Name days are not included.\n\nSend your API key in the `Authorization: Bearer` header. An invalid or expired key returns an error; the request does not switch to free access.\n\n[Terms of use](https://kalenteri.online/kayttoehdot#api-rajapinta)",
    "termsOfService": "https://kalenteri.online/kayttoehdot#api-rajapinta"
  },
  "servers": [
    {
      "url": "https://api.kalenteri.online/v1",
      "description": "Public API"
    }
  ],
  "paths": {
    "/calendar/days": {
      "get": {
        "summary": "Get calendar events",
        "description": "Returns calendar events for a date range. Without an API key, you receive the free dataset for the current year. With an API key, the dataset and available years depend on your subscription.",
        "parameters": [
          {
            "$ref": "#/components/parameters/startDate"
          },
          {
            "$ref": "#/components/parameters/endDate"
          },
          {
            "$ref": "#/components/parameters/eventType"
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "$ref": "#/components/parameters/include"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "ETag from a previous free response. Send only after the cached response has expired.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CalendarDaysResponse"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientPlan"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "304": {
            "description": "The cached free response matches the If-None-Match header. No response body. This request still counts towards the request quota."
          }
        },
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "tags": [
          "Calendar"
        ]
      }
    },
    "/calendar/day/today": {
      "get": {
        "summary": "Get today's calendar events",
        "description": "Returns today's calendar events using the time zone in Finland (Europe/Helsinki). Without an API key, you receive the free dataset. With an API key, the dataset depends on your subscription.",
        "parameters": [
          {
            "$ref": "#/components/parameters/eventType"
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "$ref": "#/components/parameters/include"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "ETag from a previous free response. Send only after the cached response has expired.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CalendarDaysResponse"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientPlan"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "304": {
            "description": "The cached free response matches the If-None-Match header. No response body. This request still counts towards the request quota."
          }
        },
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "tags": [
          "Calendar"
        ]
      }
    },
    "/calendar/day/{date}": {
      "get": {
        "summary": "Get calendar events for a date",
        "description": "Returns all calendar events available with your access level for a specific date. Without an API key, you receive the free dataset for the current year. With an API key, the dataset and available years depend on your subscription.",
        "parameters": [
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "$ref": "#/components/parameters/dayInclude"
          }
        ],
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "tags": [
          "Calendar"
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CalendarDaysResponse"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientPlan"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        }
      }
    },
    "/calendar/days/search": {
      "get": {
        "summary": "Search calendar events",
        "description": "Searches event names using a case-insensitive partial match within a date range of up to 366 days. Without an API key, the search uses the free dataset for the current year. With an API key, the dataset depends on your subscription.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Part of an event name (2–100 characters after trimming surrounding whitespace).",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            }
          },
          {
            "$ref": "#/components/parameters/startDate"
          },
          {
            "$ref": "#/components/parameters/endDate"
          },
          {
            "$ref": "#/components/parameters/eventType"
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "$ref": "#/components/parameters/include"
          }
        ],
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "tags": [
          "Calendar"
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CalendarDaysResponse"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientPlan"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        }
      }
    },
    "/calendar/moon-phases": {
      "get": {
        "summary": "Get moon phases",
        "description": "Returns moon phases for a date range. Without an API key, you can request the current year. With an API key, the available years depend on your subscription.",
        "parameters": [
          {
            "$ref": "#/components/parameters/startDate"
          },
          {
            "$ref": "#/components/parameters/endDate"
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "ETag from a previous free response. Send only after the cached response has expired.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/MoonPhasesResponse"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientPlan"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "304": {
            "description": "The cached free response matches the If-None-Match header. No response body. This request still counts towards the request quota."
          }
        },
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "tags": [
          "Calendar"
        ]
      }
    }
  },
  "components": {
    "parameters": {
      "include": {
        "name": "include",
        "in": "query",
        "description": "Comma-separated options:\n\n- `ongoing`: also includes events that started before the range and are still ongoing. Without this option, only events starting within the range are returned. With ongoing, all multi-day occurrences overlapping the range are returned once in data.ongoing, including those starting within the range. Single-day events remain in data.days. Without ongoing, data.days contains all events starting within the range and data.ongoing is empty.\n- `metadata`: adds `displayPriority`, `slug`, `shortName` and `abbreviation`; requires a paid subscription.\n- `labels`: adds Finnish display labels; requires access to the extended dataset.\n- `holidayObservances`: adds May Day Eve, Midsummer Eve, Christmas Eve and New Year's Eve; only with `type=publicHolidays`.\n- `holidaySundays`: adds Sunday observances; only with `type=publicHolidays`.\n\nFor example, `include=ongoing`, `include=metadata,ongoing` or `type=publicHolidays&include=holidayObservances,holidaySundays,metadata`. These extra fields and events are omitted unless requested. Multi-day events always include their original startDate and inclusive endDate, at every access level. Whether an eve is a day off or a shorter working day depends on factors such as the applicable collective agreement.",
        "schema": {
          "type": "string",
          "examples": [
            "metadata",
            "labels",
            "metadata,labels",
            "ongoing",
            "metadata,ongoing",
            "holidayObservances",
            "holidayObservances,holidaySundays"
          ]
        }
      },
      "format": {
        "name": "format",
        "in": "query",
        "description": "For calendar events, controls data.days: grouped (default) is an object keyed by start date; list is an array of events with a date field. data.ongoing is always an array of periods. For moon phases, controls data directly. Empty grouped values are {} and empty lists are []. List results are ordered by date and preserve event order within each date. Unsupported values return HTTP 400.",
        "schema": {
          "type": "string",
          "enum": [
            "grouped",
            "list"
          ],
          "default": "grouped"
        }
      },
      "eventType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Optionally narrows the dataset to a calendar view or event category. If omitted, returns all calendar events available with your access level. Free access and basic subscriptions include the modern and traditional calendars and all flag days, including selected theme days from the modern calendar. Access to the extended dataset also includes all theme days and unofficial church observances:\n\n- `modern`: the modern calendar, including its selected theme days; available with all access levels.\n- `traditional`: the traditional calendar.\n- `publicHolidays`: public holiday events.\n- `flagDays`: all flag days in the dataset; available with all access levels.\n- `themeDays`: the full theme-day dataset available with your subscription; requires access to the extended dataset.\n\nThe modern calendar contains the same events at every access level: theme days, unofficial observances, international celebrations and flag days selected by the shared calendar defaults. Your subscription determines the response fields and available years. Free access returns `id`, `name`, `publicHoliday` and `flagDay` for the current year. The full theme-day dataset and `themeDayTypes` classification require access to the extended dataset.\n\nAll other views are also available without an API key. `type=flagDays` includes all flag days in the dataset at every access level. Free access is limited to the current year and the base response fields. Requests with `type` are limited to 366 days; name searches always have this limit. Name days are not included in any view.",
        "schema": {
          "type": "string",
          "enum": [
            "traditional",
            "modern",
            "publicHolidays",
            "flagDays",
            "themeDays"
          ]
        }
      },
      "startDate": {
        "name": "startDate",
        "in": "query",
        "required": true,
        "description": "Start date in YYYY-MM-DD format. Without an API key, only the current year is available. With a Bearer API key, the available years depend on your subscription.",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-01-01"
      },
      "endDate": {
        "name": "endDate",
        "in": "query",
        "required": true,
        "description": "End date in YYYY-MM-DD format, on or after the start date. Without an API key, only the current year is available. With a Bearer API key, the available years depend on your subscription.",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-12-31"
      },
      "dayInclude": {
        "name": "include",
        "in": "query",
        "description": "Comma-separated options: `ongoing` also includes events that started earlier and are still ongoing on the requested date; `metadata` adds metadata with a paid subscription; `labels` adds Finnish display labels with access to the extended dataset. With ongoing, multi-day occurrences active on the requested date are returned once in data.ongoing and single-day events in data.days. Without ongoing, data.days contains all events starting on the date and data.ongoing is empty. Multi-day events always retain their original startDate and inclusive endDate.",
        "schema": {
          "type": "string",
          "examples": [
            "metadata",
            "labels",
            "metadata,labels",
            "ongoing",
            "metadata,ongoing"
          ]
        }
      }
    },
    "responses": {
      "CalendarDaysResponse": {
        "description": "Calendar events for the selected view and subscription. data contains days and ongoing. dateCount counts only distinct dates in data.days.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/CalendarDaysResponse"
            },
            "example": {
              "generatedAt": "2026-01-01T00:00:00Z",
              "dateCount": 1,
              "data": {
                "days": {
                  "2026-01-01": [
                    {
                      "id": "d64ef8e5-17d8-4b68-a127-25090e68ac43",
                      "name": "Uudenvuodenpäivä",
                      "publicHoliday": true,
                      "flagDay": null
                    }
                  ]
                },
                "ongoing": []
              },
              "source": {
                "name": "Kalenteri.online",
                "url": "https://kalenteri.online/"
              }
            }
          }
        },
        "headers": {
          "X-RateLimit-Limit-Day": {
            "description": "Daily request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Day": {
            "description": "Remaining daily requests for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Day": {
            "description": "Daily quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit-Minute": {
            "description": "Per-minute request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Minute": {
            "description": "Remaining requests in the current minute for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Minute": {
            "description": "Per-minute quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "Cache-Control": {
            "description": "Free access: private, with max-age until midnight in Finland, capped at 86400 seconds. Paid access: no-store.",
            "schema": {
              "type": "string"
            }
          },
          "Expires": {
            "description": "Cache expiry time of the free response as an HTTP date.",
            "schema": {
              "type": "string"
            }
          },
          "ETag": {
            "description": "Content identifier of the free response.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "MoonPhasesResponse": {
        "description": "Moon phases by date.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/MoonPhasesResponse"
            }
          }
        },
        "headers": {
          "X-RateLimit-Limit-Day": {
            "description": "Daily request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Day": {
            "description": "Remaining daily requests for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Day": {
            "description": "Daily quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit-Minute": {
            "description": "Per-minute request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Minute": {
            "description": "Remaining requests in the current minute for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Minute": {
            "description": "Per-minute quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "Cache-Control": {
            "description": "Free access: private, with max-age until midnight in Finland, capped at 86400 seconds. Paid access: no-store.",
            "schema": {
              "type": "string"
            }
          },
          "Expires": {
            "description": "Cache expiry time of the free response as an HTTP date.",
            "schema": {
              "type": "string"
            }
          },
          "ETag": {
            "description": "Content identifier of the free response.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid date, date range or parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "INVALID_DATE_RANGE",
                "message": "Invalid date range. 'startDate' must be before or equal to 'endDate'."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The Authorization header is malformed, or the API key is invalid, deleted or no longer has active paid access. The request does not switch to free access.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "INVALID_API_KEY",
                "message": "The API key is invalid or expired."
              }
            }
          }
        }
      },
      "InsufficientPlan": {
        "description": "Free access is limited to the current year. The subscription does not cover the requested years, or an optional parameter requires a higher access level.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "INSUFFICIENT_PLAN",
                "message": "The requested data is not included in your API access permissions."
              }
            }
          }
        }
      },
      "RateLimitExceeded": {
        "description": "Request quota exceeded. Free calendar and moon-phase requests share a quota of 100 requests per IP per day and 10 requests per IP per minute. Wait for the number of seconds specified by Retry-After. Paid access is currently limited to 1 request per 5 seconds and 10 requests per minute, per API key and IP address.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "RATE_LIMIT_EXCEEDED",
                "message": "The API request limit has been exceeded. Cache the data locally and try again later."
              }
            }
          }
        },
        "headers": {
          "X-RateLimit-Limit-Day": {
            "description": "Daily request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Day": {
            "description": "Remaining daily requests for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Day": {
            "description": "Daily quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit-Minute": {
            "description": "Per-minute request limit for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Minute": {
            "description": "Remaining requests in the current minute for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Minute": {
            "description": "Per-minute quota reset time as a Unix timestamp in seconds for free access.",
            "schema": {
              "type": "integer"
            }
          },
          "Retry-After": {
            "description": "Seconds to wait before retrying with free access.",
            "schema": {
              "type": "integer"
            }
          }
        }
      }
    },
    "schemas": {
      "CalendarDaysResponse": {
        "type": "object",
        "required": [
          "generatedAt",
          "dateCount",
          "data"
        ],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "Z$",
            "description": "Response generation time in UTC with a Z suffix. Cached responses retain the original generation time."
          },
          "dateCount": {
            "type": "integer",
            "description": "Number of distinct dates with entries in data.days. Events in data.ongoing do not contribute to this count; a response containing only ongoing periods has dateCount 0.",
            "minimum": 0
          },
          "data": {
            "type": "object",
            "description": "Calendar data. days follows the requested format; ongoing is always an array.",
            "required": [
              "days",
              "ongoing"
            ],
            "properties": {
              "days": {
                "description": "Calendar entries: grouped by start date with format=grouped, or a dated array with format=list. With include=ongoing this contains only single-day events; otherwise it contains all events starting within the requested range. Empty grouped days are {}; empty list days are [].",
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CalendarEvent"
                      }
                    }
                  },
                  {
                    "type": "array",
                    "items": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CalendarEvent"
                        },
                        {
                          "type": "object",
                          "required": [
                            "date"
                          ]
                        }
                      ]
                    }
                  }
                ]
              },
              "ongoing": {
                "type": "array",
                "description": "With include=ongoing, all multi-day occurrences overlapping the requested range, once each, ordered by original start date. This includes occurrences starting inside the range. Entries retain their full startDate and inclusive endDate and are not repeated in days. Without include=ongoing this is [].",
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CalendarEvent"
                    },
                    {
                      "type": "object",
                      "required": [
                        "startDate",
                        "endDate"
                      ],
                      "not": {
                        "required": [
                          "date"
                        ]
                      }
                    }
                  ]
                }
              }
            },
            "additionalProperties": false
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          }
        },
        "additionalProperties": false
      },
      "MoonPhasesResponse": {
        "type": "object",
        "required": [
          "generatedAt",
          "dateCount",
          "data"
        ],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "Z$",
            "description": "Response generation time in UTC with a Z suffix. Cached responses retain the original generation time."
          },
          "dateCount": {
            "type": "integer",
            "description": "Number of distinct dates in `data`."
          },
          "data": {
            "description": "`format=grouped` returns an object keyed by date; `format=list` returns an array of dated moon phases.",
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "moonPhase": {
                        "type": "string",
                        "enum": [
                          "new-moon",
                          "first-quarter",
                          "full-moon",
                          "last-quarter"
                        ]
                      }
                    },
                    "required": [
                      "moonPhase"
                    ]
                  }
                }
              },
              {
                "type": "array",
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/MoonPhasesResponse/properties/data/oneOf/0/additionalProperties/items"
                    },
                    {
                      "type": "object",
                      "required": [
                        "date"
                      ],
                      "properties": {
                        "date": {
                          "type": "string",
                          "format": "date"
                        }
                      }
                    }
                  ]
                }
              }
            ]
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "INVALID_PARAMETER",
                  "INVALID_DATE",
                  "INVALID_DATE_RANGE",
                  "DATE_RANGE_TOO_LONG",
                  "INVALID_REQUEST",
                  "INVALID_API_KEY",
                  "INSUFFICIENT_PLAN",
                  "RATE_LIMIT_EXCEEDED",
                  "INTERNAL_ERROR"
                ],
                "description": "Stable machine-readable error code. Clients must tolerate codes added in the future."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation; wording may change. Use `code` to identify the error."
              }
            }
          }
        }
      },
      "Source": {
        "type": "object",
        "description": "Included in every successful free response. When displaying the data, credit Kalenteri.online in the language of your service, for example Calendar data: Kalenteri.online. Link to the source in web environments.",
        "required": [
          "name",
          "url"
        ],
        "properties": {
          "name": {
            "type": "string",
            "const": "Kalenteri.online"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "const": "https://kalenteri.online/"
          }
        }
      },
      "CalendarEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "publicHoliday",
          "flagDay"
        ],
        "description": "All access levels include `id`, `name`, `publicHoliday` and `flagDay`. Multi-day events additionally include `startDate` and `endDate` at every access level; single-day events omit both fields. Paid subscriptions also include `countryCodes` and `classifications`. `themeDayTypes` and `labels` require access to the extended dataset.",
        "dependentRequired": {
          "startDate": ["endDate"],
          "endDate": ["startDate"]
        },
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable public UUID identifying the calendar event concept. The same ID may be shared by year-dependent versions of the event and may occur on multiple dates. It is not an identifier for a single annual occurrence or an internal database row. A single-day occurrence is identified by its date; for a multi-day occurrence, use its original startDate. The list date or grouped date key in data.days is the original start date. With include=ongoing, multi-day occurrences are returned once in data.ongoing."
          },
          "name": {
            "type": "string",
            "description": "Calendar event name in Finnish."
          },
          "startDate": {
            "type": "string",
            "format": "date",
            "description": "Original start date of a multi-day occurrence. Included together with endDate only when the event lasts more than one day, at every access level. Never clipped to the requested range."
          },
          "endDate": {
            "type": "string",
            "format": "date",
            "description": "Original last day of a multi-day occurrence, inclusive. Included together with startDate only when the event lasts more than one day, at every access level. Never clipped to the requested range."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Original start date. Present on data.days entries in list format; in grouped format it is the key in data.days. Entries in data.ongoing use startDate and endDate instead and omit date."
          },
          "countryCodes": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "uniqueItems": true,
            "description": "Associated ISO 3166-1 alpha-2 country codes in uppercase, for example FI = Finland. These do not identify the countries where the event is a statutory day off. An empty array means no country association is recorded, not that the event applies to all countries. Included with paid subscriptions."
          },
          "publicHoliday": {
            "type": "boolean",
            "description": "Whether the event is a public holiday, regardless of the day of the week. Always included as a boolean, including `false`, with all access levels. Ascension Day, Easter Sunday and Finland's Independence Day are `true`; an ordinary Sunday entry, an eve or a theme day is `false`. This does not guarantee a day off work. To identify a weekday public holiday, combine `true` with a Monday–Friday date. `type=publicHolidays` uses this classification; `holidayObservances` and `holidaySundays` may also add events with `false`."
          },
          "classifications": {
            "$ref": "#/components/schemas/CalendarClassifications"
          },
          "flagDay": {
            "$ref": "#/components/schemas/FlagDay"
          },
          "labels": {
            "$ref": "#/components/schemas/CalendarLabels"
          },
          "displayPriority": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Display priority; higher values take precedence. This field alone does not determine response order. Included with paid subscriptions when `include=metadata`. `null` means no value is recorded."
          },
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL slug. Included with paid subscriptions when `include=metadata`. `null` if not recorded."
          },
          "shortName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short name. Included with paid subscriptions when `include=metadata`. `null` if not recorded."
          },
          "abbreviation": {
            "type": [
              "string",
              "null"
            ],
            "description": "Abbreviation. Included with paid subscriptions when `include=metadata`. `null` if not recorded."
          }
        }
      },
      "CalendarClassifications": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "dayTypes",
          "churchDayTypes"
        ],
        "description": "Stable machine-readable classifications. Arrays may contain multiple values. An empty array means no classifications are recorded. Clients must tolerate values added in the future.",
        "properties": {
          "dayTypes": {
            "type": "array",
            "description": "General categories: secular holiday, celebration, event or phenomenon, memorial day, observance, church festival and folk tradition. These do not indicate membership in the traditional calendar view; use `type=traditional` for that view.",
            "items": {
              "type": "string",
              "enum": [
                "secular-holiday",
                "celebration",
                "event-or-phenomenon",
                "memorial-day",
                "observance",
                "church-festival",
                "folk-tradition"
              ]
            },
            "uniqueItems": true
          },
          "churchDayTypes": {
            "type": "array",
            "description": "Church festival, liturgical festival, special holy day, church observance and church theme day. All recorded categories are returned.",
            "items": {
              "type": "string",
              "enum": [
                "church-festival",
                "liturgical-festival",
                "special-holy-day",
                "church-observance",
                "church-theme-day"
              ]
            },
            "uniqueItems": true
          },
          "themeDayTypes": {
            "type": "array",
            "description": "Available only with the extended dataset. Theme-day categories: unofficial, United Nations, UN agency, official within the dataset, church, campaign and rare within the dataset. `official` does not mean a public holiday; `rare` is not a statistical measure of frequency.",
            "items": {
              "type": "string",
              "enum": [
                "unofficial",
                "united-nations",
                "united-nations-agency",
                "official",
                "church",
                "campaign",
                "rare"
              ]
            },
            "uniqueItems": true
          }
        }
      },
      "FlagDay": {
        "type": [
          "object",
          "null"
        ],
        "additionalProperties": false,
        "required": [
          "type",
          "flags"
        ],
        "description": "Flag-day type and flags. Included with all access levels, including free access. `null` means the event is not a flag day. This field describes an event already included in the result and does not expand the dataset. `type=flagDays` includes all flag days in the dataset at every access level; `type=modern` includes the flag days selected for that view.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "official",
              "established",
              "recommended",
              "unofficial",
              "mourning",
              "state",
              "local-or-regional"
            ],
            "description": "Official, established, recommended, unofficial or voluntary, mourning, state (such as an election), or local or regional flag day."
          },
          "flags": {
            "type": "array",
            "description": "Flags to display, not country codes. A local recommendation may have an empty array. Other flag days without explicitly recorded flags use the dataset's Finnish-flag rule.",
            "items": {
              "type": "string",
              "enum": [
                "finland",
                "aland",
                "sami",
                "tornio-valley",
                "roma",
                "european-union"
              ]
            },
            "uniqueItems": true
          }
        }
      },
      "CalendarLabels": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "language",
          "singular",
          "plural"
        ],
        "description": "Finnish display labels. Available only with the extended dataset when `include=labels`. Wording may change; use `classifications` for machine processing.",
        "properties": {
          "language": {
            "type": "string",
            "const": "fi",
            "description": "ISO 639-1 language code in lowercase. The current value `fi` means Finnish. This is a language code; `countryCodes` contains country codes such as FI = Finland."
          },
          "singular": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Display labels in the singular."
          },
          "plural": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Display labels in the plural."
          }
        }
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Paid API key in the Authorization: Bearer header. Omit the header for free access. Requests with an invalid key never switch to free access."
      }
    }
  },
  "externalDocs": {
    "description": "API examples and dataset comparison (in Finnish)",
    "url": "https://kalenteri.online/api-rajapinta"
  }
}
