{"openapi":"3.1.0","info":{"title":"Datewise API","version":"0.1.0","summary":"Business-day, working-hours & holiday computation — deterministic and versioned.","description":"Datewise computes business-day and working-hours answers over authoritative, versioned\nholiday data. Every response is deterministic and pinned to a dataset + tzdata version,\nso answers are reproducible and auditable (settlement dates, statutory deadlines, SLAs).\n\n**Determinism contract:** every response echoes `X-Dataset-Version` and `X-Tzdata-Version`.\nPass `dataset` to pin a specific frozen dataset; omit it to use the latest.\n\n**Access:** authenticated use requires an API key via the `X-API-Key` header. Get a free\nkey instantly from `POST /keys` (120 requests/minute per key). Unauthenticated (keyless)\nrequests are also permitted but rate-limited per client IP at 20 requests/minute (HTTP 429\nwhen exceeded) — this powers the public demo. An unknown key is rejected with 401; it never\ndegrades to the public tier.\n","license":{"name":"Proprietary"},"contact":{"name":"Datewise","url":"https://datewise.dev"}},"servers":[{"url":"https://datewise.dev/v1","description":"Production (API root; https://datewise.dev is the homepage)"}],"tags":[{"name":"Business days"},{"name":"Working hours"},{"name":"Holidays"},{"name":"Reference"},{"name":"Keys"}],"paths":{"/keys":{"post":{"tags":["Keys"],"operationId":"createKey","summary":"Get a free API key (self-serve, instant).","description":"Issues a free-tier API key immediately — no approval step. The key is a stateless\nsigned token: nothing about you is stored server-side, which also means a lost key\ncannot be recovered (just request a new one). Send it as `X-API-Key` on every request.\nIssuance counts against the keyless per-IP rate limit.\n","security":[{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"dev@company.com","description":"Contact email (required, not persisted by the API)."}}}}}},"responses":{"201":{"description":"Key issued.","content":{"application/json":{"schema":{"type":"object","required":["api_key","tier","rate_limit"],"properties":{"api_key":{"type":"string","example":"dw_v1.3v82Kq7w1Ap.9yD…"},"tier":{"type":"string","enum":["free"],"example":"free"},"rate_limit":{"type":"object","properties":{"requests":{"type":"integer","example":120},"period_seconds":{"type":"integer","example":60}}},"usage":{"type":"string"},"note":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/is-business-day":{"get":{"tags":["Business days"],"operationId":"isBusinessDay","summary":"Is a given date a business day?","parameters":[{"$ref":"#/components/parameters/Date"},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["date","is_business_day","meta"],"properties":{"date":{"type":"string","format":"date","example":"2026-07-03"},"is_business_day":{"type":"boolean","example":true},"reason":{"type":"string","description":"If false, why (weekend | holiday).","enum":["weekend","holiday"]},"holiday":{"$ref":"#/components/schemas/Holiday"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/add-business-days":{"get":{"tags":["Business days"],"operationId":"addBusinessDays","summary":"Add (or subtract) N business days to a date.","parameters":[{"$ref":"#/components/parameters/Date"},{"name":"days","in":"query","required":true,"description":"Business days to add; negative subtracts.","schema":{"type":"integer","example":10}},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["start_date","days","result_date","meta"],"properties":{"start_date":{"type":"string","format":"date","example":"2026-07-03"},"days":{"type":"integer","example":10},"result_date":{"type":"string","format":"date","example":"2026-07-17"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/business-days-between":{"get":{"tags":["Business days"],"operationId":"businessDaysBetween","summary":"Count business days between two dates.","description":"Counts business days in the closed interval [start, end] — inclusive of both endpoints, matching Excel's NETWORKDAYS.","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date","example":"2026-07-01"}},{"name":"end","in":"query","required":true,"schema":{"type":"string","format":"date","example":"2026-07-31"}},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["start","end","business_days","calendar_days","meta"],"properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"},"business_days":{"type":"integer","example":23},"calendar_days":{"type":"integer","example":30},"holidays_excluded":{"type":"array","items":{"$ref":"#/components/schemas/Holiday"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/next-business-day":{"get":{"tags":["Business days"],"operationId":"nextBusinessDay","summary":"Next (or previous) business day from a date.","parameters":[{"$ref":"#/components/parameters/Date"},{"name":"direction","in":"query","schema":{"type":"string","enum":["next","previous"],"default":"next"}},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","content":{"application/json":{"schema":{"type":"object","required":["from","result_date","meta"],"properties":{"from":{"type":"string","format":"date"},"direction":{"type":"string","enum":["next","previous"]},"result_date":{"type":"string","format":"date"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/is-working-hours":{"get":{"tags":["Working hours"],"operationId":"isWorkingHours","summary":"Is a timestamp within business hours (timezone-aware)?","parameters":[{"name":"timestamp","in":"query","required":true,"description":"ISO-8601 instant or zoned datetime.","schema":{"type":"string","format":"date-time","example":"2026-07-03T14:30:00-04:00"}},{"$ref":"#/components/parameters/Timezone"},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/BusinessHours"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","content":{"application/json":{"schema":{"type":"object","required":["timestamp","is_working_hours","meta"],"properties":{"timestamp":{"type":"string","format":"date-time"},"is_working_hours":{"type":"boolean"},"reason":{"type":"string","enum":["weekend","holiday","outside_hours"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/add-working-hours":{"get":{"tags":["Working hours"],"operationId":"addWorkingHours","summary":"Add working hours/minutes across DST and timezones (the core differentiator).","description":"Adds working time to a starting instant using zoned wall-clock arithmetic, skipping\nnon-working hours, weekends, and holidays, and correctly handling DST transitions\n(including gap/overlap and 30-minute-DST zones).\n","parameters":[{"name":"start","in":"query","required":true,"schema":{"type":"string","format":"date-time","example":"2026-03-08T15:00:00-05:00"}},{"name":"hours","in":"query","description":"Working hours to add (may be fractional). Negative subtracts.","schema":{"type":"number","example":8}},{"name":"minutes","in":"query","schema":{"type":"integer","example":0}},{"$ref":"#/components/parameters/Timezone"},{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"$ref":"#/components/parameters/BusinessHours"},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["start","result","meta"],"properties":{"start":{"type":"string","format":"date-time"},"hours_added":{"type":"number","example":8},"result":{"type":"string","format":"date-time","example":"2026-03-09T15:00:00-04:00"},"crossed_dst":{"type":"boolean","example":true},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/holidays":{"get":{"tags":["Holidays"],"operationId":"listHolidays","summary":"List public holidays for a country/subdivision/year.","parameters":[{"$ref":"#/components/parameters/Country"},{"$ref":"#/components/parameters/Subdivision"},{"name":"year","in":"query","required":true,"schema":{"type":"integer","example":2026}},{"$ref":"#/components/parameters/Dataset"}],"responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["country","year","holidays","meta"],"properties":{"country":{"type":"string","example":"US"},"subdivision":{"type":"string","example":"US-CA"},"year":{"type":"integer","example":2026},"holidays":{"type":"array","items":{"$ref":"#/components/schemas/Holiday"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/timezones":{"get":{"tags":["Reference"],"operationId":"listTimezones","summary":"The supported IANA timezones (the allowlist the `timezone` param validates against).","description":"The authoritative, frozen list of accepted `timezone` values, pinned to the tzdata version. Any other value returns 400 unsupported_timezone. Use this to populate a picker.","responses":{"200":{"description":"Result","headers":{"X-Dataset-Version":{"$ref":"#/components/headers/DatasetVersion"},"X-Tzdata-Version":{"$ref":"#/components/headers/TzdataVersion"}},"content":{"application/json":{"schema":{"type":"object","required":["tzdata_version","count","timezones"],"properties":{"tzdata_version":{"type":"string","example":"2026a"},"count":{"type":"integer","example":418},"timezones":{"type":"array","items":{"type":"string","example":"America/New_York"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}},"components":{"parameters":{"Date":{"name":"date","in":"query","required":true,"description":"Calendar date, ISO-8601 (YYYY-MM-DD).","schema":{"type":"string","format":"date","example":"2026-07-03"}},"Country":{"name":"country","in":"query","required":true,"description":"ISO 3166-1 alpha-2 country code.","schema":{"type":"string","example":"US"}},"Subdivision":{"name":"subdivision","in":"query","required":false,"description":"ISO 3166-2 subdivision code (e.g. US-CA, DE-BY) for regional holidays.","schema":{"type":"string","example":"US-CA"}},"Timezone":{"name":"timezone","in":"query","required":false,"description":"IANA timezone (e.g. America/New_York). Must be one of the supported zones — a frozen, versioned allowlist pinned to the tzdata version, NOT an arbitrary string. An unsupported value returns 400 unsupported_timezone. Fetch the authoritative list from GET /timezones. Defaults to the country's primary zone.","schema":{"type":"string","example":"America/New_York"}},"BusinessHours":{"name":"business_hours","in":"query","required":false,"description":"Working hours as HH:MM-HH:MM (may span midnight). Defaults to 09:00-17:00.","schema":{"type":"string","example":"09:00-17:00"}},"Dataset":{"name":"dataset","in":"query","required":false,"description":"Pin a frozen dataset version (CalVer YYYY.N). Omit for latest.","schema":{"type":"string","example":"2026.1"}}},"headers":{"DatasetVersion":{"description":"Frozen holiday dataset version used for this response.","schema":{"type":"string","example":"2026.1"}},"TzdataVersion":{"description":"IANA tzdata / ICU version used for this response.","schema":{"type":"string","example":"2026a"}}},"schemas":{"Holiday":{"type":"object","required":["date","name","type"],"properties":{"date":{"type":"string","format":"date","example":"2026-07-04"},"name":{"type":"string","example":"Independence Day"},"type":{"type":"string","enum":["public","bank","school","optional","observance"],"example":"public"},"observed":{"type":"boolean","description":"True if this is the observed (shifted) date rather than the nominal date.","example":false},"substitute_of":{"type":"string","format":"date","description":"If observed, the nominal date it substitutes."}}},"Meta":{"type":"object","required":["dataset_version","tzdata_version"],"properties":{"dataset_version":{"type":"string","example":"2026.1"},"tzdata_version":{"type":"string","example":"2026a"},"country":{"type":"string","example":"US"},"subdivision":{"type":"string","example":"US-CA"},"weekend":{"type":"array","description":"Weekday numbers treated as weekend (1=Mon..7=Sun).","items":{"type":"integer"},"example":[6,7]}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"invalid_country","description":"Machine-readable error code. This enum is the authoritative list of every code the API can emit; test/openapi.test.ts scans the source for every `badRequest(\"...\")` literal (plus not_found / internal_error) and asserts each appears here, so the contract can't drift from the implementation.","enum":["missing_country","invalid_country","invalid_subdivision","unsupported_subdivision","invalid_dataset","invalid_date","date_out_of_range","invalid_days","invalid_range","invalid_direction","invalid_year","missing_timestamp","missing_start","invalid_timestamp","invalid_timezone","unsupported_timezone","invalid_business_hours","invalid_duration","invalid_email","unsupported","unauthorized","rate_limited","not_found","keys_unavailable","internal_error"]},"message":{"type":"string","example":"Unknown country code 'XX'. Use ISO 3166-1 alpha-2."},"param":{"type":"string","example":"country"}}}}}},"responses":{"BadRequest":{"description":"Invalid parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Rate limit exceeded — keyless per-IP (get a free key via POST /keys) or free-tier per-key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServiceUnavailable":{"description":"The feature is not configured/enabled on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"security":[{},{"ApiKeyAuth":[]}]}