A holiday is a civil date in its own jurisdiction, not an instant. Christmas Day in Tokyo is 25 December in Tokyo — it does not become the 24th because you asked from Berlin. Everything below follows from that one decision.
Every date this API returns is the date as the jurisdiction itself keeps it. We do not convert it into your zone, because there is no such thing as Christmas-in-your-zone: the shops shut in Tokyo on Tokyo's 25th whatever your calendar says.
What you often need instead is the instant — when the closure
starts and ends in UTC, and which of your days it lands on. That is
what tz is for, and it adds information rather than changing
any.
Add tz to /v1/calendar. It takes an
IANA name — Asia/Tokyo, America/Sao_Paulo,
Europe/Athens. Not an offset, not an abbreviation.
curl -H "X-API-Key: $TIMESYS_KEY" \ "https://api.timesys.pro/v1/calendar?jurisdictions=JP\ &from=2027-01-01&to=2027-12-31&tz=Europe/Berlin"
Each event then carries:
| Field | What it is |
|---|---|
date | The civil date in the jurisdiction. Never shifted, with or without tz. |
starts_utc | Midnight at the start of that day, in the jurisdiction's zone, expressed in UTC. |
ends_utc | Midnight at the end of it. The closure occupies exactly this interval. |
tz | The jurisdiction's own zone — the one the interval was computed in. |
viewer_tz | The zone you passed, echoed so a stored response can be read later. |
viewer_date | The date starts_utc falls on for you. Frequently not date. |
viewer_starts | The same instant written in your zone. |
viewer_date is the useful one. A Tokyo holiday begins
at 15:00 UTC the day before. If you are in Berlin planning a call, the
question is not "what date is it in Japan" but "which of my
afternoons disappears" — and the answer is the 24th, not the 25th. That is
viewer_date. It is why the field exists and why it is not the
same as converting the date.
A zone we cannot resolve returns 400, naming what it expected:
{"detail": "unknown timezone 'CET' — expects an IANA name such as 'America/Sao_Paulo'"}
We do not quietly fall back to UTC. A caller who mistypes a zone is asking a
question we cannot answer, and answering a different one is how wrong times
reach production. CET, EST and PST are
all rejected: they are ambiguous, and several of them mean different offsets
at different times of year.
/v1/jurisdictions/{code} returns two fields, and they answer
different questions:
| Field | Meaning |
|---|---|
timezone | The presentation zone. The single zone this jurisdiction's dates are computed in. |
timezones | Every IANA zone the jurisdiction contains. The United States has 27. |
For a single-zone country the two agree and nothing is lost. For a country spanning several, the presentation zone is a choice: national holidays are national, and they need one interval rather than 27.
US-CA and US-HI both report
America/New_York; CA-BC reports
America/Toronto; AU-WA reports
Australia/Sydney.
starts_utc for those subdivisions: it is
computed from the national zone, so it can be several hours out. If you need
exact instants for a subdivision in a multi-zone country, read
timezones and compute the bound yourself for now. National-level
requests are unaffected.
| Not offered | Why |
|---|---|
UTC offsets (+02:00) | An offset is not a zone. It cannot survive a DST transition, and a calendar that spans a year will cross one. |
| Zone abbreviations | CST is North American Central, China Standard and Cuba Standard time. Three answers, six hours apart. |
| Shifting the date itself | See the top of this page. It would make the corpus wrong to make one caller's arithmetic shorter. |
| Working hours | A closure is a whole civil day here. Business hours are a separate layer and not yet published. |
"Is the Frankfurt office shut during my Tokyo morning?" Ask for
DE with tz=Asia/Tokyo and read
viewer_date.
"Store closures in a database with other timestamped events." Store
starts_utc/ends_utc and keep date
beside them. The interval sorts correctly against everything else; the date
is what a human should be shown.
"Show a user their local calendar." Pass their zone from
Intl.DateTimeFormat().resolvedOptions().timeZone — the browser
already knows it, and it is an IANA name, which is exactly what
tz wants.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; // "Europe/Athens"
const r = await fetch(`https://api.timesys.pro/v1/calendar?jurisdictions=GR`
+ `&from=2027-01-01&to=2027-12-31&tz=${encodeURIComponent(tz)}`,
{ headers: { "X-API-Key": key } });