Getting started
The API is read-only. Every endpoint is a GET, every successful response is
JSON, and the base URL is:
https://api.dagsmart.se/v3/
HTTPS only. CORS is open to every origin, so calling the API straight from a browser is fine.
Your first request
Which days are off in Sweden in 2026?
curl "https://api.dagsmart.se/v3/holidays?year=2026"
That is the whole request. There is no key to fetch first and no header to set — see Access for how the API is handed out, and why it is still worth writing to us before you build on it.
{
"year": 2026,
"count": 18,
"days": [
{
"date": "2026-01-01",
"weekday": 4,
"holidayType": "public",
"workingDay": false,
"observances": [
{
"code": "newYearsDay",
"holidayType": "public",
"name": { "sv": "nyårsdagen", "en": "New Year's Day" },
"feast": {
"code": "newYear",
"name": { "sv": "nyår", "en": "New Year" }
}
}
]
}
]
}
Eighteen, not thirteen: 2026 has thirteen public holidays and five de facto holidays, and the list carries both. A de facto holiday is a day that is not named in the Swedish Public Holidays Act but is treated as a day off anyway — Christmas Eve, New Year’s Eve, Midsummer Eve, Easter Eve and Whitsun Eve. The distinction matters, and it is covered under Classification.
Conventions
| Convention | Rule |
|---|---|
| Dates | ISO 8601, YYYY-MM-DD, always. Swedish local time decides what “today” is |
| Years | 1930–9999. year defaults to the current year in Sweden |
| Weekdays | ISO numbering: 1 is Monday, 7 is Sunday |
| Booleans | true or false, lowercase. The calendar feed is the exception |
| Field names | English. Day names are objects with language keys, { "sv": …, "en": … } |
| Lists | Wrapped in an envelope, with count beside the list |
| Single resources | Returned bare, as the object itself |
| Errors | application/problem+json, RFC 9457 — see Errors |
Lists are wrapped, single resources are not. GET /v3/holidays answers
{ "year": …, "count": …, "days": [ … ] }, while GET /v3/days/2026-06-06
answers with the day object itself. The envelope is there so that a list can
gain a field later without breaking the code you write today — read days, and
anything we add beside it will pass you by.
Unknown query parameters are ignored. Analytics parameters and hand-edited
URLs should not produce errors. Parameters we do know are validated strictly:
sundays=1 and sundays=True are both a 400, because a typo that quietly
means false is worse than one that says so. Booleans are lowercase true or
false and nothing else — watch for languages that spell them otherwise.
Access
There is nothing to sign up for. No account, no dashboard, no checkout, and no key to put in a header. Every example on this page is a complete request, and v3 answers it as it stands.
Accounts and self-service keys were the plan, and they are still the direction. They are not what v3 waited for: the API is finished and the sign-up flow is not, and holding a working API back for a login screen serves nobody. So the API ships first, and access is arranged by hand in the meantime.
Arranged by hand means a mail. Access beyond the old free endpoints is granted on request, and the request is one message to hej@dagsmart.se: what you are building, and roughly how many requests a day you expect it to make. You get an answer from a person, usually within a few days. There is no charge, because there is nothing to charge for yet.
Two things that mail is actually for — neither of them a formality:
- Room to run. v3 answers anyone, within 10 requests a minute counted per calling address — no daily or monthly ceiling. That is sized for a caller we know nothing about, and a mail is what turns you into one we do: if you share an address with an office, or need a burst wider than that, it is fixable, but only if we know. See Rate limits.
- Warning before anything changes. Keys are coming, and quotas will follow them rather than addresses. When that happens, the terms are announced in the changelog with time to act — and the integrations we know about are told directly rather than left to read it. Being one of them is the practical reason to write.
The calendar feed and the OpenAPI document ask nothing of anyone, ever. Subscribing to holidays in a calendar app should not involve a person, and generating a client should not involve one either.
The day object
One object describes a date, and it is the same object everywhere the API works one out: in a year list, in a single lookup, as a bridge day, as a half day, as every step and the result of a shift, and as one year’s occurrence of a holiday.
{
"date": "2026-06-06",
"weekday": 6,
"holidayType": "public",
"workingDay": false,
"observances": [
{
"code": "nationalDay",
"holidayType": "public",
"name": { "sv": "Sveriges nationaldag", "en": "National Day of Sweden" },
"feast": {
"code": "nationalDay",
"name": { "sv": "nationaldagen", "en": "National Day of Sweden" }
}
}
]
}
| Field | Type | Meaning |
|---|---|---|
date | string | ISO date |
weekday | integer | ISO weekday, 1–7, Monday to Sunday |
holidayType | "public", "defacto" or null | What the day is. null on an ordinary working day |
workingDay | boolean | Neither a weekend nor a holiday. True exactly when holidayType is null |
observances | array | The named holidays falling on the date. Always an array, often empty |
Bridge days and half days are working days, and they look like it:
holidayType: null, workingDay: true, observances: []. Every endpoint that
hands you a date hands you the same object, so one piece of code reads all of
them and you never have to look a date up twice to find out what it is.
workingDay answers the question most people actually have. Working it out
yourself means knowing that Sunday is a public holiday in Swedish law, which
Saturdays count, and which eves are treated as days off. The field saves you
from having to know any of it.
Classification
holidayType describes what the day is, never how you asked for it.
"public"— an allmän helgdag under the Swedish Public Holidays Act (1989:253). Sundays are public holidays in their own right under that Act and are classified as such whether or not you asked for Sundays."defacto"— not named in the Act, but a day off in practice. Saturdays, and the five eves.null— an ordinary working day.
De facto does not mean “without support in law”. Midsummer Eve, Christmas Eve and New Year’s Eve are equated with Sunday by the Annual Leave Act (1977:480) s. 3 a and by the Act on the Calculation of Statutory Time (1930:173) s. 2. Only Easter Eve and Whitsun Eve rest on custom alone, and both always fall on a Saturday anyway. The opposite of de facto here is named in the Act, not legal. There is a fuller explanation, in Swedish, on de facto-helgdagar.
When two holidays share a date, the stronger one decides. 6 June 2071 is
both the National Day (public) and Whitsun Eve (de facto); the day is public,
which is also what the law says. That is the whole reason holidayType sits on
the day and not only on each observance.
Because classification is unconditional, GET /v3/days/{date} takes no flags at
all. There is nothing to choose.
Observances
observances lists the named holidays falling on the date.
| Field | Meaning |
|---|---|
code | Stable identifier for the observance, e.g. christmasEve. Names may be reworded; codes may not |
holidayType | "public" or "defacto", for this observance |
name | The observance’s own name, per language |
feast | The feast it belongs to, as { code, name } |
The feast is the occasion; the observance is the day. Christmas Eve, Christmas
Day and Boxing Day are three observances of one feast, christmas. So is
Epiphany, which is unexpected in English but obvious in Swedish, where the day
is called trettondedag jul — the thirteenth day of Christmas.
Saturdays and Sundays are never observances. The Act reads “public holidays
are Sundays and: New Year’s Day, …” — two different mechanisms in one
sentence. The named days are holidays; the weekend is a property of the date.
Nobody celebrates Sunday. The rule for a consumer is therefore: name the day
from observances, and if the list is empty the day is off because of the
weekday, which weekday gives you.
observances is always an array — with one element, with two, or with none.
It is never absent and never null, so you can loop over it without checking
first.
Endpoints
GET /v3/holidays
Public and de facto holidays for a year, one entry per date.
| Parameter | In | Default | Meaning |
|---|---|---|---|
year | query | current year | 1930–9999 |
sundays | query | false | Also list every Sunday of the year |
saturdays | query | false | Also list every Saturday |
sundays and saturdays are independent, and both take exactly true or
false. They are separate because Sundays are public holidays in law and
Saturdays are not: someone asking the legally precise question should be able to
ask it precisely.
The flags decide which days are listed, never what a day is. Turning
sundays on adds the year’s remaining Sundays as entries with an empty
observances list and holidayType: "public". It does not change how Easter
Sunday is classified — Easter Sunday was already a public holiday.
curl "https://api.dagsmart.se/v3/holidays?year=2026&saturdays=true&sundays=true"
{
"year": 2026,
"count": 114,
"days": [
{
"date": "2026-01-01",
"weekday": 4,
"holidayType": "public",
"workingDay": false,
"observances": ["…"]
},
{
"date": "2026-01-03",
"weekday": 6,
"holidayType": "defacto",
"workingDay": false,
"observances": []
}
]
}
GET /v3/bridge-days
Bridge days — klämdagar — for a year: the working days wedged between a holiday and a weekend, which is when Swedish offices go quiet on a Friday nobody formally has off.
| Parameter | In | Default | Meaning |
|---|---|---|---|
year | query | current year | 1930–9999 |
curl "https://api.dagsmart.se/v3/bridge-days?year=2026"
{
"year": 2026,
"count": 3,
"days": [
{
"date": "2026-01-02",
"weekday": 5,
"holidayType": null,
"workingDay": true,
"observances": []
}
]
}
GET /v3/half-days
Half days — halvdagar — for a year: working days before a public holiday, when many workplaces finish early. A convention, not an entitlement.
| Parameter | In | Default | Meaning |
|---|---|---|---|
year | query | current year | 1930–9999 |
curl "https://api.dagsmart.se/v3/half-days?year=2026"
2026 has five — 2 April, 30 April, 13 May, 5 June and 30 October — each returned as an ordinary day object.
GET /v3/days/{date}
Looks up a single date.
| Parameter | In | Required | Meaning |
|---|---|---|---|
date | path | yes | The date to look up, ISO 8601 |
There are no query parameters — not even the weekend flags; see Classification for why there is nothing to choose.
An ordinary working day is not an error. It comes back with a null classification and no observances: the date exists, it simply is not a holiday.
curl "https://api.dagsmart.se/v3/days/2026-06-08"
{
"date": "2026-06-08",
"weekday": 1,
"holidayType": null,
"workingDay": true,
"observances": []
}
A date that is not a date — 2026-02-30, 2026-13-01 — is a 400, not a
404. It was never a resource to begin with.
GET /v3/period
Everything we know about the days between two dates, in numbers: how many there are, how they read as calendar units, how many are working days, and how the days off divide up.
| Parameter | In | Default | Meaning |
|---|---|---|---|
from | query | — | First date of the period |
to | query | — | Last date of the period |
bounds | query | inclusive | Which endpoints the period counts |
curl "https://api.dagsmart.se/v3/period?from=2026-03-01&to=2026-03-31"
{
"from": "2026-03-01",
"to": "2026-03-31",
"bounds": "inclusive",
"counted": { "first": "2026-03-01", "last": "2026-03-31" },
"calendarDays": 31,
"duration": { "years": 0, "months": 1, "days": 0 },
"workingDays": 22,
"daysOff": {
"total": 9,
"publicHolidays": 5,
"defactoHolidays": 4,
"namedDays": 0,
"weekendDays": 9
},
"bridgeDays": 0,
"halfDays": 0
}
| Field | Type | Meaning |
|---|---|---|
from, to | string | The dates you asked about, echoed back |
bounds | string | The reading applied, whether you asked for it or got the default |
counted | object or null | The days the reading actually covers. null when the period is empty |
calendarDays | integer | Days in the period |
duration | object | The same period as calendar units: years, months, days |
workingDays | integer | Days that are neither weekend nor holiday |
daysOff | object | The rest, split two ways — see below |
bridgeDays | integer | Working days wedged between a holiday and a weekend |
halfDays | integer | Days customarily worked as half days |
“Between two dates” has four readings, and you pick one.
bounds | Counts | The sentence it matches |
|---|---|---|
inclusive (default) | both dates | “How many working days are there in March?” |
exclusive | neither | “How many days lie between them?” |
from | the start date only | “How long until the 31st?” |
to | the end date only | “How long since the 1st?” |
The difference is one day at each end — the kind that ships to production and is
discovered in a payroll run. So the reading is a parameter rather than a guess,
it governs every number in the response, and counted tells you which days
that turned out to be. Two identical dates with bounds=exclusive contain no
days: calendarDays is 0 and counted is null, never minus one.
Because one reading governs the whole response, the numbers add up:
workingDays + daysOff.total = calendarDays, and daysOff.total splits two
ways — by law into publicHolidays and defactoHolidays, by what the day is
into namedDays and weekendDays. Both pairs sum to the same total. Sundays
are counted as public holidays and Saturdays as de facto ones, because that is
what they are; a Sunday that is also Easter Sunday is one day, counted once.
duration is the calendar reading of the same period — the way a statutory
deadline is counted under s. 1 of the Act on the Calculation of Statutory Time
(1930:173). One month from 1 January is 1 February, whatever February is worth
in days. It is here because you cannot derive it from a day count. Weeks are
not, because you can.
Numbers, not rows. The endpoint says how many named holidays fall in the
period, never which ones. For the list, ask for the years you need — the day
lists are what /v3/holidays is for.
from after to is a 400. The API will not quietly swap them for you. A
period longer than 200 years is also a 400: that is the span the summary is
computed over.
GET /v3/shift
What date is one month and three working days from here? Steps are applied in the order given, and a chain is sent whole rather than one request per step — the Sunday rule below is why that matters.
| Parameter | In | Default | Meaning |
|---|---|---|---|
date | query | — | The date to move from |
add | query | — | A step. Repeat the parameter, or comma-separate, for a chain — order is preserved |
postpone | query | false | Apply the Sunday rule |
A step is an amount and a unit:
| Unit | Means |
|---|---|
d | days |
b | working days |
w | weeks |
m | months |
y | years |
Negative moves backwards: -3b is three working days back. At most 20 steps.
Repeating the parameter is the canonical form, but a comma-separated list works
too — that is the form a shared link from the calculator carries,
and it should paste straight into a request.
curl "https://api.dagsmart.se/v3/shift?date=2026-01-30&add=1m&add=-2d"
{
"date": "2026-01-30",
"postpone": false,
"steps": [
{
"unit": "m",
"amount": 1,
"constrained": true,
"result": { "date": "2026-02-28", "weekday": 6, "…": "…" }
},
{
"unit": "d",
"amount": -2,
"constrained": false,
"result": { "date": "2026-02-26", "weekday": 4, "…": "…" }
}
],
"landed": { "date": "2026-02-26", "…": "…" },
"postponed": false,
"result": { "date": "2026-02-26", "…": "…" },
"period": { "from": "2026-01-30", "to": "2026-02-26", "calendarDays": 28, "…": "…" }
}
| Field | Type | Meaning |
|---|---|---|
date | string | The date you asked about, echoed back |
postpone | boolean | Whether the Sunday rule was asked for |
steps | array | The chain in the order applied. Each step carries its unit, amount, a constrained flag and the day it landed on |
landed | day object | Where the steps ended up, before the rule had its say |
postponed | boolean | Whether the rule actually moved the date |
result | day object | The answer |
period | object or null | The span between date and result |
Order changes the answer. +1 month, +30 days and +30 days, +1 month are
different dates, so every step reports its own result and they are applied in
the order given. constrained marks the step where month arithmetic had to give
way: 31 January plus one month is 28 February, because 31 February does not
exist.
The Sunday rule is a choice, and it applies once. Under s. 2 of the same Act,
a deadline landing on a day that is not a working day moves to the next one.
postpone=true applies it to the chain, after the last step — landed is where
the steps ended up and result is the answer, with postponed saying whether
the rule moved anything.
Once, not per step, and the difference is not small. Take 30 January 2026 and
add=1m&add=-2d:
| Result | |
|---|---|
| The rule applied once, at the end | 26 February — the steps land on Thursday the 26th, a working day |
| The rule applied to each step | 2 March — 28 February is a Saturday, so it jumps to Monday, and two days back is Saturday again |
Splitting the chain across requests and passing postpone on each one produces
the second answer. Sending the chain in one request is how you get the first.
period describes the span between date and result with the same numbers
/v3/period returns — minus the reading, since a shift’s period always counts
both ends. It is null when that span exceeds 200 years
— the period here is a consequence of the shift rather than the question, so an
enormous one leaves the summary out rather than making the whole request an
error.
A zero step leaves the date untouched, even if it is a holiday. Zero working days
from Christmas Day is Christmas Day; rounding to the next working day would be an
opinion, and an unasked-for one. If you want it rounded, that is what postpone
is for.
A step that carries the date outside 1930–9999 is a 400, and so is a chain that
cannot be read — a step is a whole number and one of the five units, lowercase,
with no leading +, and there are at most 20 of them. add=1m&add=-2d and
add=1m,-2d are the same chain.
GET /v3/observances
The catalogue: every named holiday the API knows, with the years it applied. Takes no parameters.
The catalogue cannot be derived from any single year. Annunciation Day stopped being a holiday in 1952 and Whit Monday in 2004, so neither appears in a modern year at all — and the National Day only became one in 2005.
{
"count": 20,
"observances": [
{
"code": "nationalDay",
"holidayType": "public",
"name": { "sv": "Sveriges nationaldag", "en": "National Day of Sweden" },
"feast": {
"code": "nationalDay",
"name": { "sv": "nationaldagen", "en": "National Day of Sweden" }
},
"since": 2005
}
]
}
since and until appear only where they apply: since is the first year the
day was a holiday, until the last. A code with neither has been one throughout
the range the API covers. The full list is under
Observance codes.
GET /v3/observances/{code}
When does a given holiday fall — this year, or across a run of years?
| Parameter | In | Default | Meaning |
|---|---|---|---|
code | path | — | Observance code, as listed by /v3/observances |
from | query | current year | First year |
to | query | same as from | Last year |
to before from is a 400, and so is a range spanning more than 200 years.
curl "https://api.dagsmart.se/v3/observances/ascensionDay?from=2026&to=2030"
{
"code": "ascensionDay",
"holidayType": "public",
"name": { "sv": "Kristi himmelsfärdsdag", "en": "Ascension Day" },
"feast": {
"code": "ascension",
"name": { "sv": "Kristi himmelsfärdsdag", "en": "Ascension" }
},
"count": 5,
"occurrences": [
{ "year": 2026, "day": { "date": "2026-05-14", "…": "…" } },
{ "year": 2027, "day": { "date": "2027-05-06", "…": "…" } }
]
}
Years when the day was not a holiday are left out, so the series can be
shorter than the range you asked for, and count can legitimately be 0.
Asking for whitMonday in 2026 returns the observance, its until: 2004 and an
empty list. That is an answer, not a miss — which is why the range is not
validated against the observance’s own lifetime.
An unknown code is a 404.
Observance codes
Twenty of them. The sv and en names are for display; the code is what you
should store.
| Code | Type | English name | Feast | Applied |
|---|---|---|---|---|
newYearsDay | public | New Year’s Day | newYear | |
epiphany | public | Epiphany | christmas | |
annunciation | public | Annunciation Day | annunciation | until 1952 |
goodFriday | public | Good Friday | easter | |
easterSunday | public | Easter Sunday | easter | |
easterMonday | public | Easter Monday | easter | |
ascensionDay | public | Ascension Day | ascension | |
mayDay | public | May Day | mayDay | from 1939 |
whitsun | public | Whit Sunday | pentecost | |
whitMonday | public | Whit Monday | pentecost | until 2004 |
nationalDay | public | National Day of Sweden | nationalDay | from 2005 |
midsummerDay | public | Midsummer Day | midsummer | |
allSaintsDay | public | All Saints’ Day | allSaints | from 1953 |
christmasDay | public | Christmas Day | christmas | |
boxingDay | public | Boxing Day | christmas | |
holySaturday | defacto | Easter Eve | easter | |
whitsunEve | defacto | Whitsun Eve | pentecost | |
midsummerEve | defacto | Midsummer Eve | midsummer | |
christmasEve | defacto | Christmas Eve | christmas | |
newYearsEve | defacto | New Year’s Eve | newYear |
The English names are the established ones in English-language listings of Swedish holidays rather than literal translations: Boxing Day rather than “Second Day of Christmas”, Epiphany rather than “Thirteenth Day of Christmas”. Swedish and English sit side by side on helgdagarnas engelska namn.
Calendar feed (iCal)
An iCalendar subscription for calendar apps, for people rather than for code:
https://api.dagsmart.se/ical/svenska-helgdagar.ics
Nothing to arrange, and no version in the path. Point a calendar app at the address once and it keeps working — that is the whole promise, and it is why the feed sits outside the versioned API.
https://api.dagsmart.se/ical/holidays serves the same calendar, and both
addresses are permanent. Prefer the .ics one: some calendar apps want to see a
file extension before they will accept a URL at all.
The feed covers the current year ±2, and that window cannot be changed. For other years, use the API.
What the calendar contains is up to you. Six query parameters, each taking
true or false:
| Parameter | Default | Effect |
|---|---|---|
defacto | true | Includes de facto holidays |
bridge | false | Includes bridge days |
half | false | Includes half days |
saturdays | false | Includes every Saturday |
sundays | false | Includes every Sunday |
busy | false | Marks the events as busy time rather than free |
Leave them out and you get the default calendar: public holidays and de facto
holidays, nothing else. The calendar’s name follows the choices —
Svenska helgdagar + klämdagar – Dagsmart — and cannot be set separately.
Unknown parameters are ignored; a non-boolean value is a 400. Booleans are
generous about spelling (true, 1, on, yes, or a bare ?bridge), since
these URLs get edited by hand.
The subscription page builds the URL for you with checkboxes.
Errors
Errors are RFC 9457 problem documents, served as application/problem+json:
{
"type": "https://dagsmart.se/api/problems/invalid-parameter",
"title": "Invalid parameter",
"status": 400,
"detail": "year: Too small: expected number to be >=1930"
}
Branch on type, never on detail. The type URI is stable per kind of
error and is what you should match. title and detail are written for humans
and may be reworded without notice.
type | Status | When |
|---|---|---|
…/problems/invalid-parameter | 400 | A parameter failed validation, or a range ends before it starts |
…/problems/invalid-date | 400 | Right format, impossible date — 2026-02-30 |
…/problems/invalid-year | 400 | year malformed or outside 1930–9999, on the v2 endpoints |
…/problems/not-found | 404 | No such route, or no observance with that code |
…/problems/rate-limited | 429 | Too many requests in the window. Retry-After says how long to wait |
…/problems/gone | 410 | A retired endpoint. Reserved for the v2 sunset |
…/problems/internal-error | 500 | Our fault. Nothing a different request will fix |
The prefix is https://dagsmart.se/api/problems/ throughout. The URIs are
identifiers rather than pages: they name the error, and this table is where they
are documented.
Caching
Every answer here is a function of the parameters you sent. 2026 had the same
holidays yesterday and will have them tomorrow, so responses are cached at the
edge and carry an ETag.
| Response | Cache-Control | Why |
|---|---|---|
| A year you named, a date, a period, a shift, the catalogue, this API’s schema | public, max-age=0, s-maxage=2592000, stale-while-revalidate=31536000 | Determined entirely by the request. It cannot go stale |
The same endpoints with year left out | public, max-age=0, s-maxage= seconds until midnight in Sweden | The answer fell back on today, and today ends |
/health | no-store | A health check answered from a cache is not a health check |
Errors are never cached. Neither is anything but a 200.
max-age=0 is deliberate. Your own cache is not something we can clear, and
ours is. Should we ever ship a wrong serialisation, we would rather purge it
than wait a month for it to expire in your process. If you do want to hold a
response locally, hold it against its ETag.
Conditional requests are cheap. Send the ETag back as If-None-Match and
you get a 304 with no body when nothing has changed:
curl -H 'If-None-Match: "3f1c8a90b2d5e714"' \
'https://api.dagsmart.se/v3/holidays?year=2026'
The calendar feed has its own arrangement — six hours, and a limit that stops generation rather than delivery. See Calendar feed.
Rate limits
One limit, counted per calling address — there is no key to count it per, and no monthly quota at all:
| Surface | Limit |
|---|---|
/v3/* | 10 requests per minute |
| The old v2 endpoints | 5 requests per 10 seconds |
| Calendar feed | 30 requests per 10 seconds, or 10 with parameters |
There is no cap on how much you may fetch over a day or a month. The limit is there to keep one caller from crowding out the rest, not to meter you. Pull the years you need — the whole catalogue, 1930 to 2180, is a few hundred calls.
⚠️ v3 is slower than the old endpoints, deliberately. Those allow 5 requests per 10 seconds, which is 30 a minute; v3 allows 10. The number is the one a caller without a paid plan will have once keys exist, applied from the start, so that it does not drop out from under anything built in the meantime. Pace a bulk fetch accordingly, or write to us — a caller who needs more is a solvable problem, and a mail is how it gets solved.
v3 is deliberately no stricter than v2. The old endpoints allowed 5 requests per 10 seconds — 30 a minute — so v3 allows the same. Migrating should never cost you room, and the wind-down would be a strange thing to run while making the destination worse than the origin.
An address is a blunt instrument, and we know it. An office behind one firewall counts as one caller, and so does a mobile network. That is the price of having no keys yet; if it is the wrong answer for what you are building, write to us and we will sort it out by hand.
For the calendar feed, the limit applies only when a response has to be built: a cached one is handed out regardless. A calendar app refreshing twice a day never comes close, and neither does an office full of them behind one address.
Responses report the limit rather than making you infer it:
| Header | On | Meaning |
|---|---|---|
RateLimit-Limit | every response | Requests allowed in the window |
RateLimit-Policy | every response | The policy, as limit;w=seconds |
Retry-After | 429 | Seconds to wait before retrying |
There is no RateLimit-Remaining header. Rather than pace yourself against a
running count, handle the 429 when it comes and wait the number of seconds
Retry-After gives you.
The limits fail open: if the counter behind them breaks, your request goes through. A protection that hiccups should cost us capacity, never you.
There will not be a monthly quota later either. Capacity here is sold as pace, not as volume: what a plan buys is how fast you may ask, never a total you may fetch. A monthly ceiling lands weeks after the traffic that spent it, falls on whoever shares your address, and adds nothing a per-minute limit does not already do. Plan against the number above and nothing else.
Migrating from v2
The old endpoints answer today, and will for a while yet. This is what changes when you move.
Address and access
| v2 | v3 | |
|---|---|---|
| Base URL | https://api.dagsmart.se/ | https://api.dagsmart.se/v3/ |
| Sign-up | none | none — see Access |
| Pace | 5 requests / 10 s per address (30 a minute), undocumented | 10 a minute per address, documented — see Rate limits |
| Errors | the router’s default shape | RFC 9457 application/problem+json |
Administratively the move costs you nothing: no account to open, no key to provision, no ticket to wait on. Change the base URL and read the new shape. The only reason to be in touch is the one under Access — so that we know you exist before keys and quotas do.
It does cost you pace, and that is the one thing to plan for. v3 allows a third of what the old endpoints do. The number is the one that applies once keys exist, applied now so it will not drop later; there is still no limit on how much you fetch in total, only on how fast. A loop that walked twenty years back-to-back against v2 needs to spread out against v3 — or tell us, and we will sort it.
The response shape
This is the big one, and the only change that really asks something of you.
v2 returns one entry per holiday. When two holidays land on the same date the date comes back twice, which happens ten times between 1930 and 2100 — among them 1 May 2008 (Ascension Day and May Day) and 6 June 2049, 2055 and 2060 (Whit Sunday and the National Day).
v2:
[
{ "date": "2008-05-01", "code": "ascensionDay", "name": { "sv": "…", "en": "…" } },
{ "date": "2008-05-01", "code": "mayDay", "name": { "sv": "…", "en": "…" } }
]
v3:
{
"year": 2008,
"count": 17,
"days": [
{
"date": "2008-05-01",
"weekday": 4,
"holidayType": "public",
"workingDay": false,
"observances": [
{ "code": "ascensionDay", "…": "…" },
{ "code": "mayDay", "…": "…" }
]
}
]
}
So: the array moved inside an envelope, and each element became a day carrying a
list of holidays. If all you want is the dates, read days[].date and ignore
the rest. If you were counting entries to count days off, v2 was giving you a
number one too high in those years.
Weekend flags
| v2 | v3 | |
|---|---|---|
| Flag | weekends=true | sundays=true and saturdays=true, independent |
The codes sunday and saturday | in the response | gone — the weekday is a property, not a holiday |
| Weekday | derive it from the date yourself | weekday on every day, ISO 1–7 |
weekends=true maps to both new flags.
Watch out if you read name from the first entry for a date. In v2 that could
be söndag rather than påskdagen, depending on whether the holiday sat in the
public list or the de facto one, and the ordering between the two was
effectively arbitrary. In v3 the problem cannot arise, because weekdays are
never observances.
Fields that are new
| Field | v2 | v3 |
|---|---|---|
holidayType | — | on the day and on every observance |
workingDay | — | on the day. True exactly when holidayType is null |
feast | — | on every observance, as { code, name } |
weekday | — | on the day |
count | — | on every list envelope |
On v2 there was no way to tell a public holiday from a de facto one. That is probably the most useful thing you gain by moving.
Endpoints that are new
None of these existed in v2, so there is nothing to migrate. They are simply available once you are on v3.
| Endpoint | Answers |
|---|---|
GET /v3/days/{date} | What is this one date? |
GET /v3/period | How many days, working days and days off lie between two dates? |
GET /v3/shift | What date is one month and three working days from here? |
GET /v3/observances | Which holidays exist, and which years did they apply? |
GET /v3/observances/{code} | When does this holiday fall, over a run of years? |
Corrections to the data
These change the answer, not the format. If you diff v2 against v3 you will see them, and they are deliberate.
Easter fell on the wrong date in 1954, 1981, 2049 and 2076. v2 placed it seven days late, and for 1981 and 2076 on 26 April — a date Easter cannot fall on at all. Each of those years moves Good Friday, Easter Eve, Easter Sunday, Easter Monday, Ascension Day, Whitsun Eve and Whit Sunday, plus a bridge day and two or three half days. 2049 and 2076 are still ahead of us, so this is a difference you can hit in production, not only in a backfill.
Duplicate entries removed. v2 could return the same bridge day or half day twice: 7 June in 2019, 2030, 2041 and 2052, and the half days 24 March 1932 and 30 April 2008. The two entries were identical, so nothing is lost.
One English name corrected. whitsun now reads Whit Sunday rather than a
lowercase whitsun, which also pairs it with Whit Monday. No other name
changed.
The corrections reach the old endpoints too. They return the same shape as before, with the dates put right. Keeping v2 on Easter dates we know to be wrong would have been the worse of the two options.
What has not changed
- The calendar feed. Same URL, same response, nothing to arrange. See Calendar feed.
- The year range, 1930–9999, with the current year as the default.
- CORS, open to all origins.
- Date format, ISO 8601.
The v2 endpoints
These three are the previous version of the API: no version in the path, served from the root, and returning one entry per holiday rather than one per day. v3 replaces them, and they are being withdrawn on the schedule under The wind-down below.
Until then they keep answering, and they are frozen: same responses, same behaviour, bug fixes only. Nothing built on them stops working before the sunset date. Everything new is built in v3 alone — Migrating from v2 is the way across.
| Endpoint | Parameters | Response |
|---|---|---|
GET /holidays | year, weekends | [{ date, code, name }] |
GET /half-days | year | [{ date }] |
GET /bridge-days | year | [{ date }] |
Both parameters are optional:
| Parameter | In | Default | Accepts |
|---|---|---|---|
year | query | current year | Exactly four digits, 1930–9999 |
weekends | query | false | true, in any casing. Adds the year’s Saturdays and Sundays. /holidays only |
year has to be four digits and nothing else, so year=26, year=2026-01-01
and a bare year= are each an …/problems/invalid-year rather than
a fallback to the current year. Leaving year out is the only way to get the
default. Any value of weekends other than true reads as false.
Every response carries a Link header pointing here, with both
rel="deprecation" and rel="sunset". Once the dates are fixed, two more
headers carry them:
| Header | Format | Example |
|---|---|---|
Deprecation | RFC 9745 — a structured field date: @ and seconds since the epoch | Deprecation: @1788220800 |
Sunset | RFC 8594 — an HTTP date | Sunset: Mon, 01 Mar 2027 00:00:00 GMT |
The two formats differ, and neither is a typo. Sunset predates structured
fields; Deprecation was standardised in 2025 and uses them. Parse each by its
own rules — reading the Deprecation line as an HTTP date gets you nothing.
Deprecation is the moment the endpoints were declared obsolete, not the moment
they stop working. That is what Sunset is for, and it never comes earlier.
Neither of those two appears until the dates are set. Until then the Link
header is the only one of the three you will see.
The wind-down
The endpoints are withdrawn in steps rather than switched off on a single date, so that the change is noticeable while there is still time to act on it:
| Step | What happens |
|---|---|
| Today | The Link header points here. The endpoints are frozen, and no end date is set |
| The announcement | Deprecation and Sunset join it, carrying dates, announced in the changelog. The clock below starts here |
| +1 month | The limit on the old endpoints tightens below the 5 per 10 seconds they have today |
| +3 months | It tightens again |
| −1 month | Brownouts: announced windows, an hour at a time, where v2 answers 410 |
| Sunset (+6 months) | The old endpoints answer 410 Gone permanently, with a problem document pointing here |
The six months run from the announcement, not from v3’s launch. How long
these endpoints need to keep answering depends on who is still calling them and
how hard, and that is not knowable until v3 has been serving them for a while.
Setting the date on launch day would be a guess dressed as a commitment. Until
the announcement lands there is no deadline to miss, and the Link header —
the one signal that reaches a consumer who never reads this page — is on every
response from day one.
The final status is 410 Gone and not 404: the endpoints existed, and the
answer says they were withdrawn rather than that they were never there.
Plan around the brownouts. They are short and announced in advance, and they are the last clear signal before the endpoints stop answering for good: a request that fails during one will fail permanently a month later.
Reference
OpenAPI. The specification is generated from the running API, so it always describes what you actually get:
https://api.dagsmart.se/v3/openapi.json
It is OpenAPI 3.1, and it is the file to point a client generator at.
Health. GET https://api.dagsmart.se/health reports whether the API is up,
and which version of the calendar data it is answering from. It is for
monitoring, not for data.
Changes. Anything that affects consumers — a new field, the sunset date, the day keys and quotas arrive — is announced in the changelog, which is written in Swedish. If you have been in touch, you also hear it from us directly.
Wrong data? A holiday on the wrong date is a bug we want to know about, and there is precedent: see the Easter correction above.