Hoppa till innehållet
Dagsmart.

Dagsmart API

Reliable data on the Swedish calendar, as JSON. Everything about version 3 is on this page – endpoints, fields, errors, limits – and so is what you need to move off version 2.

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

ConventionRule
DatesISO 8601, YYYY-MM-DD, always. Swedish local time decides what “today” is
Years1930–9999. year defaults to the current year in Sweden
WeekdaysISO numbering: 1 is Monday, 7 is Sunday
Booleanstrue or false, lowercase. The calendar feed is the exception
Field namesEnglish. Day names are objects with language keys, { "sv": …, "en": … }
ListsWrapped in an envelope, with count beside the list
Single resourcesReturned bare, as the object itself
Errorsapplication/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" }
      }
    }
  ]
}
FieldTypeMeaning
datestringISO date
weekdayintegerISO weekday, 1–7, Monday to Sunday
holidayType"public", "defacto" or nullWhat the day is. null on an ordinary working day
workingDaybooleanNeither a weekend nor a holiday. True exactly when holidayType is null
observancesarrayThe 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.

FieldMeaning
codeStable identifier for the observance, e.g. christmasEve. Names may be reworded; codes may not
holidayType"public" or "defacto", for this observance
nameThe observance’s own name, per language
feastThe 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.

ParameterInDefaultMeaning
yearquerycurrent year1930–9999
sundaysqueryfalseAlso list every Sunday of the year
saturdaysqueryfalseAlso 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.

ParameterInDefaultMeaning
yearquerycurrent year1930–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.

ParameterInDefaultMeaning
yearquerycurrent year1930–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.

ParameterInRequiredMeaning
datepathyesThe 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.

ParameterInDefaultMeaning
fromqueryFirst date of the period
toqueryLast date of the period
boundsqueryinclusiveWhich 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
}
FieldTypeMeaning
from, tostringThe dates you asked about, echoed back
boundsstringThe reading applied, whether you asked for it or got the default
countedobject or nullThe days the reading actually covers. null when the period is empty
calendarDaysintegerDays in the period
durationobjectThe same period as calendar units: years, months, days
workingDaysintegerDays that are neither weekend nor holiday
daysOffobjectThe rest, split two ways — see below
bridgeDaysintegerWorking days wedged between a holiday and a weekend
halfDaysintegerDays customarily worked as half days

“Between two dates” has four readings, and you pick one.

boundsCountsThe sentence it matches
inclusive (default)both dates“How many working days are there in March?”
exclusiveneither“How many days lie between them?”
fromthe start date only“How long until the 31st?”
tothe 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.

ParameterInDefaultMeaning
datequeryThe date to move from
addqueryA step. Repeat the parameter, or comma-separate, for a chain — order is preserved
postponequeryfalseApply the Sunday rule

A step is an amount and a unit:

UnitMeans
ddays
bworking days
wweeks
mmonths
yyears

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, "…": "…" }
}
FieldTypeMeaning
datestringThe date you asked about, echoed back
postponebooleanWhether the Sunday rule was asked for
stepsarrayThe chain in the order applied. Each step carries its unit, amount, a constrained flag and the day it landed on
landedday objectWhere the steps ended up, before the rule had its say
postponedbooleanWhether the rule actually moved the date
resultday objectThe answer
periodobject or nullThe 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 end26 February — the steps land on Thursday the 26th, a working day
The rule applied to each step2 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?

ParameterInDefaultMeaning
codepathObservance code, as listed by /v3/observances
fromquerycurrent yearFirst year
toquerysame as fromLast 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.

CodeTypeEnglish nameFeastApplied
newYearsDaypublicNew Year’s DaynewYear
epiphanypublicEpiphanychristmas
annunciationpublicAnnunciation Dayannunciationuntil 1952
goodFridaypublicGood Fridayeaster
easterSundaypublicEaster Sundayeaster
easterMondaypublicEaster Mondayeaster
ascensionDaypublicAscension Dayascension
mayDaypublicMay DaymayDayfrom 1939
whitsunpublicWhit Sundaypentecost
whitMondaypublicWhit Mondaypentecostuntil 2004
nationalDaypublicNational Day of SwedennationalDayfrom 2005
midsummerDaypublicMidsummer Daymidsummer
allSaintsDaypublicAll Saints’ DayallSaintsfrom 1953
christmasDaypublicChristmas Daychristmas
boxingDaypublicBoxing Daychristmas
holySaturdaydefactoEaster Eveeaster
whitsunEvedefactoWhitsun Evepentecost
midsummerEvedefactoMidsummer Evemidsummer
christmasEvedefactoChristmas Evechristmas
newYearsEvedefactoNew Year’s EvenewYear

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:

ParameterDefaultEffect
defactotrueIncludes de facto holidays
bridgefalseIncludes bridge days
halffalseIncludes half days
saturdaysfalseIncludes every Saturday
sundaysfalseIncludes every Sunday
busyfalseMarks 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.

typeStatusWhen
…/problems/invalid-parameter400A parameter failed validation, or a range ends before it starts
…/problems/invalid-date400Right format, impossible date — 2026-02-30
…/problems/invalid-year400year malformed or outside 1930–9999, on the v2 endpoints
…/problems/not-found404No such route, or no observance with that code
…/problems/rate-limited429Too many requests in the window. Retry-After says how long to wait
…/problems/gone410A retired endpoint. Reserved for the v2 sunset
…/problems/internal-error500Our 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.

ResponseCache-ControlWhy
A year you named, a date, a period, a shift, the catalogue, this API’s schemapublic, max-age=0, s-maxage=2592000, stale-while-revalidate=31536000Determined entirely by the request. It cannot go stale
The same endpoints with year left outpublic, max-age=0, s-maxage= seconds until midnight in SwedenThe answer fell back on today, and today ends
/healthno-storeA 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:

SurfaceLimit
/v3/*10 requests per minute
The old v2 endpoints5 requests per 10 seconds
Calendar feed30 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:

HeaderOnMeaning
RateLimit-Limitevery responseRequests allowed in the window
RateLimit-Policyevery responseThe policy, as limit;w=seconds
Retry-After429Seconds 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

v2v3
Base URLhttps://api.dagsmart.se/https://api.dagsmart.se/v3/
Sign-upnonenone — see Access
Pace5 requests / 10 s per address (30 a minute), undocumented10 a minute per address, documented — see Rate limits
Errorsthe router’s default shapeRFC 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

v2v3
Flagweekends=truesundays=true and saturdays=true, independent
The codes sunday and saturdayin the responsegone — the weekday is a property, not a holiday
Weekdayderive it from the date yourselfweekday 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

Fieldv2v3
holidayTypeon the day and on every observance
workingDayon the day. True exactly when holidayType is null
feaston every observance, as { code, name }
weekdayon the day
counton 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.

EndpointAnswers
GET /v3/days/{date}What is this one date?
GET /v3/periodHow many days, working days and days off lie between two dates?
GET /v3/shiftWhat date is one month and three working days from here?
GET /v3/observancesWhich 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.

EndpointParametersResponse
GET /holidaysyear, weekends[{ date, code, name }]
GET /half-daysyear[{ date }]
GET /bridge-daysyear[{ date }]

Both parameters are optional:

ParameterInDefaultAccepts
yearquerycurrent yearExactly four digits, 1930–9999
weekendsqueryfalsetrue, 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:

HeaderFormatExample
DeprecationRFC 9745 — a structured field date: @ and seconds since the epochDeprecation: @1788220800
SunsetRFC 8594 — an HTTP dateSunset: 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:

StepWhat happens
TodayThe Link header points here. The endpoints are frozen, and no end date is set
The announcementDeprecation and Sunset join it, carrying dates, announced in the changelog. The clock below starts here
+1 monthThe limit on the old endpoints tightens below the 5 per 10 seconds they have today
+3 monthsIt tightens again
−1 monthBrownouts: 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.