API

Date math over HTTP: the business-day counts, date differences, and public-holiday calendars the calculators answer, served as JSON over plain GET requests.

curl "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=US"

The API is in early access. No key is needed, and use is free within the rate limits each response declares; the interim rule is in the terms.

This page is generated from the machine-readable OpenAPI description at openapi.json; the examples below are tested against the live API.

Conventions

Business-day and calendar-day date math and public-holiday calendars. Successful responses use ISO-8601 dates and include a warnings array for non-fatal notices; errors are RFC 9457 application/problem+json documents with a machine-readable code. Holiday data is compiled by the open-source date-holidays project (https://github.com/commenthol/date-holidays) and used under a Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0, https://creativecommons.org/licenses/by-sa/3.0/); a consumer that redistributes this data inherits the same obligation to attribute and to share adaptations under the same license (ShareAlike). See /about for full attributions. Early access: free to use within the rate limits each response declares (RateLimit and RateLimit-Policy headers). No compatibility promise yet. The API's own terms and license are coming and will be posted at whendays.com/terms.

Count the business days between two dates

GET /api/v1/business-days

Counts the working days from start_date to end_date for the given jurisdiction, skipping weekends and that jurisdiction's public holidays. include_start / include_end choose whether each endpoint is counted. Counting is whole-day: dates bearing only partial-day holidays (a half-day closing such as Germany's Christmas Eve) count as working days. The range must not exceed 10 years; a wider range is refused as invalid_input. Business-day answers cover only jurisdictions with verified holiday data, currently US, GB (subdivision required), DE, FR, IL; any other country is refused as invalid_input rather than guessed. A subdivision-required country without a certified subdivision is refused as subdivision_required, with offered_subdivisions listing the certified choices. Every success carries scope (the certified national_class, for a labeled national answer its scope_label, and, where a region observes statutory holidays the national set omits, a regional_scope naming what the answer covers and excludes), so a consumer can render the answer's jurisdiction scope without owning the registry. Certification is Whendays' own verification against each jurisdiction's official governing records; per-jurisdiction method and dates are published at whendays.com/coverage.

Parameters

ParameterTypeDescription
start_datestring
^\d{4}-\d{2}-\d{2}$
end_datestring
^\d{4}-\d{2}-\d{2}$
include_startstring, optional
true | false, default true
include_endstring, optional
true | false, default true
countrystringISO 3166-1 alpha-2 country code (e.g. US, GB). Case-insensitive. The holiday dataset carries 199 countries and territories; business-day operations answer only the 5 verified jurisdictions (see each operation's description).
subdivisionstring, optionalISO 3166-2 subdivision code element, read together with country (e.g. CA for US-CA). A business-day answer for GB requires one — the national calendar alone cannot answer.

Response

The business-day count and the range it was computed over.

MemberTypeConstraints
start_datestring^\d{4}-\d{2}-\d{2}$
end_datestring^\d{4}-\d{2}-\d{2}$
include_startboolean
include_endboolean
business_daysinteger>= 0
warningsarray of strings
jurisdiction object
MemberTypeConstraints
jurisdiction.countrystring^[A-Z]{2}$
jurisdiction.subdivisionstring or null^[A-Z0-9]{1,3}$
scope object
MemberTypeConstraints
scope.national_classstringleaf | floor | none
scope.scope_labelstring or null
scope.regional_scopeobject or null
scope.regional_scope.coversstring
scope.regional_scope.excludesstring

Examples

curl
curl "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=US"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=US",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/business-days",
    params={
        "start_date": "2026-01-01",
        "end_date": "2026-01-31",
        "country": "US",
    },
)
data = response.json()

Returns HTTP 200.

curl
curl "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=GB"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=GB",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/business-days",
    params={
        "start_date": "2026-01-01",
        "end_date": "2026-01-31",
        "country": "GB",
    },
)
data = response.json()

Refused with HTTP 400, code subdivision_required.

Shift a date by a number of business days

GET /api/v1/business-days/shift

Returns the date days business days after (direction: add) or before (direction: subtract) start_date for the given jurisdiction; the start date itself is never counted. Counting is whole-day: dates bearing only partial-day holidays (a half-day closing such as Germany's Christmas Eve) count as working days. Business-day answers cover only jurisdictions with verified holiday data, currently US, GB (subdivision required), DE, FR, IL; any other country is refused as invalid_input rather than guessed. A subdivision-required country without a certified subdivision is refused as subdivision_required, with offered_subdivisions listing the certified choices. Every success carries scope (the certified national_class, for a labeled national answer its scope_label, and, where a region observes statutory holidays the national set omits, a regional_scope naming what the answer covers and excludes), so a consumer can render the answer's jurisdiction scope without owning the registry. Certification is Whendays' own verification against each jurisdiction's official governing records; per-jurisdiction method and dates are published at whendays.com/coverage.

Parameters

ParameterTypeDescription
directionstring
add | subtract
start_datestring
^\d{4}-\d{2}-\d{2}$
daysinteger
> 0, <= 100000
countrystringISO 3166-1 alpha-2 country code (e.g. US, GB). Case-insensitive. The holiday dataset carries 199 countries and territories; business-day operations answer only the 5 verified jurisdictions (see each operation's description).
subdivisionstring, optionalISO 3166-2 subdivision code element, read together with country (e.g. CA for US-CA). A business-day answer for GB requires one — the national calendar alone cannot answer.

Response

The landing date and the direction the shift moved.

MemberTypeConstraints
directionstringadd | subtract
start_datestring^\d{4}-\d{2}-\d{2}$
landing_datestring^\d{4}-\d{2}-\d{2}$
business_daysinteger> 0
warningsarray of strings
jurisdiction object
MemberTypeConstraints
jurisdiction.countrystring^[A-Z]{2}$
jurisdiction.subdivisionstring or null^[A-Z0-9]{1,3}$
scope object
MemberTypeConstraints
scope.national_classstringleaf | floor | none
scope.scope_labelstring or null
scope.regional_scopeobject or null
scope.regional_scope.coversstring
scope.regional_scope.excludesstring

Example

curl
curl "https://www.whendays.com/api/v1/business-days/shift?direction=add&start_date=2026-01-01&days=5&country=US"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/business-days/shift?direction=add&start_date=2026-01-01&days=5&country=US",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/business-days/shift",
    params={
        "direction": "add",
        "start_date": "2026-01-01",
        "days": "5",
        "country": "US",
    },
)
data = response.json()

Returns HTTP 200.

Count the calendar days between two dates

GET /api/v1/date-difference

Counts the calendar days from start_date to end_date. Jurisdiction-free — a pure date difference needs no country.

Parameters

ParameterTypeDescription
start_datestring
^\d{4}-\d{2}-\d{2}$
end_datestring
^\d{4}-\d{2}-\d{2}$

Response

The calendar-day count and the range it was computed over.

MemberTypeConstraints
start_datestring^\d{4}-\d{2}-\d{2}$
end_datestring^\d{4}-\d{2}-\d{2}$
daysinteger>= 0
warningsarray of strings

Example

curl
curl "https://www.whendays.com/api/v1/date-difference?start_date=2026-01-01&end_date=2026-01-31"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/date-difference?start_date=2026-01-01&end_date=2026-01-31",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/date-difference",
    params={
        "start_date": "2026-01-01",
        "end_date": "2026-01-31",
    },
)
data = response.json()

Returns HTTP 200.

Shift a date by a number of calendar days

GET /api/v1/date-difference/shift

Returns the date days calendar days after (direction: add) or before (direction: subtract) start_date. Jurisdiction-free.

Parameters

ParameterTypeDescription
directionstring
add | subtract
start_datestring
^\d{4}-\d{2}-\d{2}$
daysinteger
> 0, <= 1000000

Response

The landing date and the direction the shift moved.

MemberTypeConstraints
directionstringadd | subtract
start_datestring^\d{4}-\d{2}-\d{2}$
landing_datestring^\d{4}-\d{2}-\d{2}$
daysinteger> 0
warningsarray of strings

Example

curl
curl "https://www.whendays.com/api/v1/date-difference/shift?direction=add&start_date=2026-01-01&days=5"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/date-difference/shift?direction=add&start_date=2026-01-01&days=5",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/date-difference/shift",
    params={
        "direction": "add",
        "start_date": "2026-01-01",
        "days": "5",
    },
)
data = response.json()

Returns HTTP 200.

List a jurisdiction's public holidays for a year

GET /api/v1/holiday-calendar

Lists the public holidays for the given jurisdiction and year, each with its ISO date, name, and type. Open to the 199 countries and territories the holiday dataset carries; year is answerable from 1900 to 2200.

Parameters

ParameterTypeDescription
yearstring
^\d{4}$
countrystringISO 3166-1 alpha-2 country code (e.g. US, GB). Case-insensitive. The holiday dataset carries 199 countries and territories; business-day operations answer only the 5 verified jurisdictions (see each operation's description).
subdivisionstring, optionalISO 3166-2 subdivision code element, read together with country (e.g. CA for US-CA). A business-day answer for GB requires one — the national calendar alone cannot answer.

Response

The holiday list for the jurisdiction and year.

MemberTypeConstraints
yearinteger>= 1900, <= 2200
warningsarray of strings
jurisdiction object
MemberTypeConstraints
jurisdiction.countrystring^[A-Z]{2}$
jurisdiction.subdivisionstring or null^[A-Z0-9]{1,3}$
holidays[] array of objects
MemberTypeConstraints
holidays[].datestring^\d{4}-\d{2}-\d{2}$
holidays[].namestring
holidays[].typestring

Example

curl
curl "https://www.whendays.com/api/v1/holiday-calendar?year=2026&country=US&subdivision=CA"
JavaScript
const response = await fetch(
  "https://www.whendays.com/api/v1/holiday-calendar?year=2026&country=US&subdivision=CA",
);
const data = await response.json();
Python
import requests

response = requests.get(
    "https://www.whendays.com/api/v1/holiday-calendar",
    params={
        "year": "2026",
        "country": "US",
        "subdivision": "CA",
    },
)
data = response.json()

Returns HTTP 200.

Errors

An error is an application/problem+json document, never a bare status page. Branch on its code, one of:

  • validation_error
  • jurisdiction_required
  • subdivision_required
  • invalid_input
  • not_found
  • rate_limited
  • internal_error

Machine-readable discriminator a client branches on. A client MUST treat an unrecognized code as a generic failure of the response's HTTP status — new codes are additive and never a break.

HTTP statusWhen
400The request is malformed or unanswerable as posed. code is one of validation_error, jurisdiction_required, subdivision_required (a certified subdivision must be chosen; offered_subdivisions lists the choices), or invalid_input.
429The per-IP rate limit was exceeded (code: rate_limited).
500An unexpected server fault (code: internal_error).

The problem document

MemberTypeConstraints
typestring
titlestring
statusinteger>= 400, <= 599
codestringvalidation_error | jurisdiction_required | subdivision_required | invalid_input | not_found | rate_limited | internal_error
detailstring, optional
instancestring, optional
offered_subdivisionsarray of strings, optional
details[] array of objects, optional
MemberTypeConstraints
details[].fieldstring
details[].messagestring

Rate limits

The live numbers are declared, not documented: each response carries its current limit in the RateLimit and RateLimit-Policy headers, and a 429 adds Retry-After with the seconds to wait. Read the limits from the headers, not from a page.

HeaderMeaning
RateLimitCurrent rate-limit state, following the IETF RateLimit header draft-07: limit=<n>, remaining=<n>, reset=<seconds> (e.g. limit=60, remaining=59, reset=30). Informational — this header tracks a moving IETF draft and its format may change, so parse defensively and use the stable Retry-After (returned on 429) for backoff. Spec: https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07
RateLimit-PolicyThe rate-limit policy in effect (IETF RateLimit-Policy draft-07): <limit>;w=<window-seconds>, e.g. 60;w=60 — 60 requests per 60 seconds. Informational, with the same draft caveat as RateLimit.
X-Correlation-IdThe per-request correlation id, echoed on every response so a successful call ties to a log line; the same id is the problem instance on errors.
Retry-AfterSeconds to wait before retrying, per RFC 9110 §10.2.3. Present on 429.

Jurisdiction coverage

What each operation answers for is stated in its description above, derived from the same registry the calculators use. The Coverage page lists each supported jurisdiction with the method and date of its verification; the About page cites the governing legal records the holiday data is checked against.

Terms

API use is governed by the interim API clause in the site terms.