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
| Parameter | Type | Description |
|---|---|---|
start_date | string^\d{4}-\d{2}-\d{2}$ | |
end_date | string^\d{4}-\d{2}-\d{2}$ | |
include_start | string, optionaltrue | false, default true | |
include_end | string, optionaltrue | false, default true | |
country | string | ISO 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). |
subdivision | string, optional | ISO 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.
| Member | Type | Constraints |
|---|---|---|
start_date | string | ^\d{4}-\d{2}-\d{2}$ |
end_date | string | ^\d{4}-\d{2}-\d{2}$ |
include_start | boolean | |
include_end | boolean | |
business_days | integer | >= 0 |
warnings | array of strings |
jurisdiction
object
| Member | Type | Constraints |
|---|---|---|
jurisdiction.country | string | ^[A-Z]{2}$ |
jurisdiction.subdivision | string or null | ^[A-Z0-9]{1,3}$ |
scope
object
| Member | Type | Constraints |
|---|---|---|
scope.national_class | string | leaf | floor | none |
scope.scope_label | string or null | |
scope.regional_scope | object or null | |
scope.regional_scope.covers | string | |
scope.regional_scope.excludes | string |
Examples
curl "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=US"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();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 "https://www.whendays.com/api/v1/business-days?start_date=2026-01-01&end_date=2026-01-31&country=GB"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();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
| Parameter | Type | Description |
|---|---|---|
direction | stringadd | subtract | |
start_date | string^\d{4}-\d{2}-\d{2}$ | |
days | integer> 0, <= 100000 | |
country | string | ISO 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). |
subdivision | string, optional | ISO 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.
| Member | Type | Constraints |
|---|---|---|
direction | string | add | subtract |
start_date | string | ^\d{4}-\d{2}-\d{2}$ |
landing_date | string | ^\d{4}-\d{2}-\d{2}$ |
business_days | integer | > 0 |
warnings | array of strings |
jurisdiction
object
| Member | Type | Constraints |
|---|---|---|
jurisdiction.country | string | ^[A-Z]{2}$ |
jurisdiction.subdivision | string or null | ^[A-Z0-9]{1,3}$ |
scope
object
| Member | Type | Constraints |
|---|---|---|
scope.national_class | string | leaf | floor | none |
scope.scope_label | string or null | |
scope.regional_scope | object or null | |
scope.regional_scope.covers | string | |
scope.regional_scope.excludes | string |
Example
curl "https://www.whendays.com/api/v1/business-days/shift?direction=add&start_date=2026-01-01&days=5&country=US"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();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
| Parameter | Type | Description |
|---|---|---|
start_date | string^\d{4}-\d{2}-\d{2}$ | |
end_date | string^\d{4}-\d{2}-\d{2}$ |
Response
The calendar-day count and the range it was computed over.
| Member | Type | Constraints |
|---|---|---|
start_date | string | ^\d{4}-\d{2}-\d{2}$ |
end_date | string | ^\d{4}-\d{2}-\d{2}$ |
days | integer | >= 0 |
warnings | array of strings |
Example
curl "https://www.whendays.com/api/v1/date-difference?start_date=2026-01-01&end_date=2026-01-31"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();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
| Parameter | Type | Description |
|---|---|---|
direction | stringadd | subtract | |
start_date | string^\d{4}-\d{2}-\d{2}$ | |
days | integer> 0, <= 1000000 |
Response
The landing date and the direction the shift moved.
| Member | Type | Constraints |
|---|---|---|
direction | string | add | subtract |
start_date | string | ^\d{4}-\d{2}-\d{2}$ |
landing_date | string | ^\d{4}-\d{2}-\d{2}$ |
days | integer | > 0 |
warnings | array of strings |
Example
curl "https://www.whendays.com/api/v1/date-difference/shift?direction=add&start_date=2026-01-01&days=5"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();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
| Parameter | Type | Description |
|---|---|---|
year | string^\d{4}$ | |
country | string | ISO 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). |
subdivision | string, optional | ISO 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.
| Member | Type | Constraints |
|---|---|---|
year | integer | >= 1900, <= 2200 |
warnings | array of strings |
jurisdiction
object
| Member | Type | Constraints |
|---|---|---|
jurisdiction.country | string | ^[A-Z]{2}$ |
jurisdiction.subdivision | string or null | ^[A-Z0-9]{1,3}$ |
holidays[]
array of objects
| Member | Type | Constraints |
|---|---|---|
holidays[].date | string | ^\d{4}-\d{2}-\d{2}$ |
holidays[].name | string | |
holidays[].type | string |
Example
curl "https://www.whendays.com/api/v1/holiday-calendar?year=2026&country=US&subdivision=CA"const response = await fetch(
"https://www.whendays.com/api/v1/holiday-calendar?year=2026&country=US&subdivision=CA",
);
const data = await response.json();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_errorjurisdiction_requiredsubdivision_requiredinvalid_inputnot_foundrate_limitedinternal_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 status | When |
|---|---|
400 | The 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. |
429 | The per-IP rate limit was exceeded (code: rate_limited). |
500 | An unexpected server fault (code: internal_error). |
The problem document
| Member | Type | Constraints |
|---|---|---|
type | string | |
title | string | |
status | integer | >= 400, <= 599 |
code | string | validation_error | jurisdiction_required | subdivision_required | invalid_input | not_found | rate_limited | internal_error |
detail | string, optional | |
instance | string, optional | |
offered_subdivisions | array of strings, optional |
details[]
array of objects, optional
| Member | Type | Constraints |
|---|---|---|
details[].field | string | |
details[].message | string |
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.
| Header | Meaning |
|---|---|
RateLimit | Current 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-Policy | The 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-Id | The 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-After | Seconds 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.