One key reaches every endpoint. What it limits is which territories you may ask about — never how often you ask, and never which routes you may call.
There is no per-endpoint permission and nothing to enable. The same key answers a market calendar and a national one:
curl -H "X-API-Key: $TIMESYS_API_KEY" \ "https://api.timesys.pro/v1/calendar?jurisdictions=ES&from=2027-01-01&to=2027-12-31" curl -H "X-API-Key: $TIMESYS_API_KEY" \ "https://api.timesys.pro/v1/exchanges/XETR/calendar?from=2027-01-01&to=2027-12-31"
The header is X-API-Key, not Authorization:
Bearer. That is the single commonest integration error, and it
returns 401 with a message saying so.
In an environment variable read at start-up, on the server. Nowhere else.
# .env — never committed TIMESYS_API_KEY=ts_live_xxxxxxxxxxxxxxxxxxxx
// Node
const res = await fetch(
`https://api.timesys.pro/v1/calendar?jurisdictions=ES&from=${from}&to=${to}`,
{ headers: { "X-API-Key": process.env.TIMESYS_API_KEY } });
# Python
requests.get(url, headers={"X-API-Key": os.environ["TIMESYS_API_KEY"]})
A key in front-end JavaScript is a public key, whatever the file is called. Anyone can read it from the network tab, and it carries your whole territory scope. If your front end needs this data, call us from your server and serve the result from your own endpoint — which you want anyway, because you should be caching.
The same goes for a mobile app: a key compiled into a binary is a key somebody can extract. Put it behind your own API.
Rotate it. POST /v1/account/rotate, authenticated by the key
being replaced, mints a new one and revokes the old in the same
transaction. Your plan, your Stripe subscription and every territory you
have claimed carry over, so nothing you have deployed loses scope — only
the string changes.
| Limited | Not limited |
|---|---|
| Which territories you may ask about | How many calls you make |
| Whether subdivisions are included | Which endpoints you may call |
| How far ahead a free key may look | How much data per response |
Ask for a territory outside your scope and the answer is 403 naming the territory and the plan that would cover it — not a silent empty list. An empty list means we hold nothing; a 403 means you may not see it. Those are different answers and the Calendar API never conflates them.
A calendar range is capped at 400 days. Split a multi-year sync by year. This is not a rate limit — it bounds one response so a five-year request cannot quietly become a fifteen-megabyte one.
A bare country code returns national rows only. Asking for
GB gives you the United Kingdom's national calendar and not
Scotland's. If you serve English users, ask for GB-ENG. This
caught our own first customer: syncing GB silently dropped
Easter Monday and the Summer Bank Holiday for England.
Calls are unlimited on every plan, and the reason is that this data changes on a published schedule rather than continuously. Editions are monthly. A holiday calendar for 2027 is the same answer today and next Tuesday.
Fetch on a schedule, store the result, and read from your own store. A
payroll system checking holidays quarterly makes roughly forty thousand
calls a year if it polls and about twelve if it caches — and both cost the
same, which is the point. rate_per_minute exists as abuse
protection; hitting it returns 429 telling you to slow down, and never an
invoice.
Every row carries assurance: verified against the
instrument, corroborated by two independent sources, or
compiled from a published rule. It is a field you can branch on, and
it is the reason to use this rather than a holiday list.
// Do not print an estimate as a hard date if (row.assurance === "verified" || row.precision === "exact") render(row.date); else renderWithCaveat(row);
Filter server-side with min_assurance=verified if you only
want dates confirmed against an authority.
Calendar responses do not carry an edition string today — that is worth
saying plainly, because it is the sort of thing people assume. What you can
record is GET /v1/health, which returns the running
version and db_mtime, the corpus build the answer
came from.
{"status":"ok","version":"v1.36.1","observances":3990,
"keystore":"ok","db_mtime":1787131781}
If you are producing something dated — a report, a filing, a printed planner — store those two alongside the dates. Dossiers are different: each PDF carries its own edition on the cover, and the edition you cited stays retrievable.
| Code | Means | Do |
|---|---|---|
401 | No key, wrong header, or a rotated key | Check it is X-API-Key |
403 | Outside your territory scope | The message names the plan that covers it |
400 | Range over 400 days, or a malformed date | Split by year; dates are YYYY-MM-DD |
404 | Unknown jurisdiction or exchange code | Check against /v1/jurisdictions |
429 | Abuse protection, not billing | Back off; you are almost certainly not caching |
The configurator assembles a request against the live corpus, shows the response, and gives you the URL to copy. Signed in on a paid plan, Use my key mints a temporary key for that browser — separate from yours, expiring in an hour, so nothing you have deployed is touched.