List public holidays by country and year for payroll

Call the holiday endpoint once for each country and year you pay in. Every entry has a scope and a list of subdivision_codes, so payroll can separate nationwide holidays from regional ones and from collective leave days, then apply its own rules to each group.

Get a nationwide list for one country

GET /v1/calendars/holidays/{country}/{year} returns the holidays of a country for a year. The optional scope parameter keeps one kind of holiday. This request asks for Indonesia in 2026 and keeps only national_public.

curl, Indonesia's national public holidays
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/calendars/holidays/IDN/2026?scope=national_public"
Response
{
  "country": "IDN",
  "coverage": [
    {
      "scope": "national_public",
      "subdivision_codes": []
    }
  ],
  "holidays": [
    {
      "date": "2026-01-01",
      "name": "New Year’s Day",
      "holiday_key": "new_years_day",
      "local_name": "Tahun Baru 2026 Masehi",
      "scope": "national_public",
      "subdivision_codes": []
    },
    {
      "date": "2026-01-16",
      "name": "Isra and Mi’raj",
      "holiday_key": "isra_and_miraj",
      "local_name": "Isra Mikraj Nabi Muhammad S.A.W.",
      "scope": "national_public",
      "subdivision_codes": []
    }
  ]
}

The coverage list says what the calendar contains: one scope, national_public, with an empty subdivision_codes list. An empty list on an entry means the holiday applies nationwide. local_name carries the name in the local language when one is known. The full response holds 17 dates.

Get a regional list for another country

Not every holiday is national. This request asks for Australia in 2026 without a scope.

curl, Australia's holidays for a year
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/calendars/holidays/AUS/2026"
Response
{
  "country": "AUS",
  "coverage": [
    {
      "scope": "bank",
      "subdivision_codes": [
        "AU-NSW"
      ]
    },
    {
      "scope": "regional_public",
      "subdivision_codes": [
        "AU-NSW"
      ]
    }
  ],
  "holidays": [
    {
      "date": "2026-01-01",
      "name": "New Year’s Day",
      "holiday_key": "new_years_day",
      "scope": "regional_public",
      "subdivision_codes": [
        "AU-NSW"
      ]
    },
    {
      "date": "2026-01-26",
      "name": "Australia Day",
      "holiday_key": "australia_day",
      "scope": "regional_public",
      "subdivision_codes": [
        "AU-NSW"
      ]
    }
  ]
}

Here every entry carries subdivision_codes of AU-NSW, and coverage lists the scopes bank and regional_public, both for AU-NSW only. The calendar covers New South Wales. It holds nothing for other Australian states, so a payroll with staff in Victoria needs another source for them. Entries also carry observed: Additional Day for Boxing Day on 28 December is a substitute weekday for 26 December, which falls on a Saturday.

Count the holidays that apply to each employee

  1. Get an API key Get an API key at signup and keep it in NARWHAL_API_KEY.
  2. Pair each country with the employee's subdivision The script pairs IDN with no subdivision and AUS with AU-NSW. Use the subdivision from the employee record.
  3. Keep the entries that apply and group them by scope An entry applies when its subdivision_codes list is empty or contains the employee's subdivision. The script groups the dates by scope and counts how many fall on weekdays, since weekend holidays do not remove a working day.
    Python, holidays per scope for two countries
    import os
    from collections import defaultdict
    from datetime import date
    import requests
    
    HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}
    
    
    def year_list(country, year):
        r = requests.get(
            f"https://api.narwhalapi.com/v1/calendars/holidays/{country}/{year}",
            headers=HEADERS,
            timeout=10,
        )
        r.raise_for_status()
        return r.json()
    
    
    for country, region in (("IDN", None), ("AUS", "AU-NSW")):
        data = year_list(country, 2026)
        print(country, [(c["scope"], c["subdivision_codes"]) for c in data["coverage"]])
        dates = defaultdict(set)  # scope -> dates that apply to this employee
        for h in data["holidays"]:
            if not h["subdivision_codes"] or region in h["subdivision_codes"]:
                dates[h["scope"]].add(h["date"])
        for scope, days in sorted(dates.items()):
            weekdays = [d for d in days if date.fromisoformat(d).weekday() < 5]
            print("  ", scope, len(days), "dates,", len(weekdays), "on weekdays")
Output of the script
IDN [('collective_leave', []), ('national_public', [])]
   collective_leave 8 dates, 8 on weekdays
   national_public 17 dates, 13 on weekdays
AUS [('bank', ['AU-NSW']), ('regional_public', ['AU-NSW'])]
   bank 1 dates, 1 on weekdays
   regional_public 13 dates, 9 on weekdays
CountryScopeDatesOn weekdays
Indonesianational_public1713
Indonesiacollective_leave88
Australia, New South Walesregional_public139
Australia, New South Walesbank11

The weekday count is the number to compare with a payroll calendar. Indonesia has 21 weekday dates across both scopes, and New South Wales has 10. Whether a collective_leave day or a bank holiday is paid leave in your contracts is your policy and local rules to decide. The API labels each day and does not decide pay.

Worked example: Eid al-Fitr week in Indonesia

DateWeekdaynamescope
2026-03-20FridayEid al-Fitrcollective_leave
2026-03-21SaturdayEid al-Fitrnational_public
2026-03-22SundayEid al-Fitrnational_public
2026-03-23MondayEid al-Fitrcollective_leave
2026-03-24TuesdayEid al-Fitrcollective_leave

Both national_public dates fall on a weekend, so a request with scope=national_public shows no lost working day that week. The three weekdays, Friday 20, Monday 23 and Tuesday 24 March, appear only under collective_leave. A payroll that filtered to national holidays would miss them. This is why the script reads the whole list and groups by scope.

What do the scopes mean for payroll?

national_public is a holiday for the whole country. collective_leave holds Indonesia's cuti bersama, extra days the calendar lists on their own scope. regional_public and bank entries carry subdivision codes that say where they apply.

Request the year without scope and group by it in code, as the script does, so you see every kind of day. Add scope to the request only when one payroll rule needs just that kind.

When a rule refers to one particular holiday, key it on holiday_key and not on name. Within a country the key names one holiday, so a rule for christmas_day picks out that one holiday in the returned list, while the name is display text that can vary by country. The same key is used for the same kind of day in other countries, which helps when one policy covers several.

How do I get only the holidays for one state or region?

The year endpoint has no subdivision parameter, so filter in code: keep entries whose subdivision_codes is empty or contains the code, such as AU-NSW. Read coverage first, because it shows which subdivisions the calendar covers at all.

What happens when a country has no holidays for a scope?

The API returns a 404 problem response with the code calendar_not_published, and its detail names the country, year and requested scope. A request for the United Kingdom with scope=regional_public returned it, because the United Kingdom's entries use the bank scope. Request without scope to see which scopes exist.

Published .

Next steps