For utviklere

Det åpne API-et til denne kalenderen, hvordan du bygger den inn på ditt nettsted, og nøklene som identifiserer applikasjonen din.

API

The public GraphQL API of Tysnes Kalender. Read the overview first if you have not; the full type list is on the reference page and in schema.graphql.

Endpoint

POST https://hvaskjer-staging.web.app/graphQL
Content-Type: application/json

{"query": "...", "variables": {...}, "operationName": "..."}
  • variables and operationName are optional. Responses are JSON: {"data": ..., "errors": [...], "extensions": {...}}.
  • GET is accepted for queries (?query=...&variables=..., URL-encoded) but only with the header Apollo-Require-Preflight: true. Without it the server answers 400 with a CSRF message. Prefer POST.
  • CORS is open: a browser page on any origin may call the endpoint directly. Custom headers cause a preflight, which is answered.
  • Opening the endpoint in a browser tab (a request that accepts HTML) shows a sandbox with the schema, autocompletion and a runnable query. Introspection is enabled.
  • There is no versioning in the URL. Changes are announced on the changelog and, for keyed callers, by e-mail before they land.

Headers

Header / parameter Who sends it Meaning
X-Api-Key: hsk_… you Your application's key. Identity, not security: see Keys.
?key=hsk_… you, when headers are out of reach The same key as a query-string parameter, for platforms that cannot set headers.
X-Page-Url: https://… browser embeds The full URL of the page the embed is mounted on. Browsers only send the origin as Referer cross-origin, so without this a widget on example.no/kultur/program is indistinguishable from any other page on example.no.
X-Client-Id: name/version our own clients Reserved for the calendar's own front end, its server render, the widget and the screen apps. Do not send it: it would file your traffic under ours.

A key that is unknown or revoked does not fail the request: it is served as anonymous and the response carries an extensions.notice saying so.

The events query

query Upcoming($page: Int, $pageSize: Int, $filter: Filter) {
  events(page: $page, pageSize: $pageSize, filter: $filter) {
    totalCount
    hasMore
    pageInfo { currentPage pageSize totalPages }
    data {
      id
      event_slug
      eventLink
      title_nb
      title_en
      startDate
      endDate
      startTime
      duration
      categories
      mode
      venue { id name slug address location { latitude longitude } }
      organizers { id name slug website }
      images { urlSmall urlLarge alt }
      repetitions { startDate endDate startTime venue { name } }
    }
  }
}

Pagination

events(filter, page, pageSize). page starts at 0; pageSize defaults to 10. Read the totals from the EventConnection, not from the length of data:

  • totalCount — events matching the filter, across all pages;
  • hasMore — whether page + 1 has anything;
  • pageInfo — currentPage, pageSize, totalPages.

page < 0 or pageSize <= 0 is a BAD_USER_INPUT error. The connection also carries facets over the whole filtered set (not just the page): venues, organizers and categories, each an array of { …, hits }.

Filter

All fields are optional. Combine freely; every condition must hold.

Field Meaning
fromDate Events still current at this instant or later: not yet ended, or (for events published without an end time, where endDate == startDate) started less than three hours before. Defaults to now.
untilDate Events that start before this instant.
fromStartDate Events that start at this instant or later. Use this rather than fromDate when ongoing events must not appear.
categories Category ids (see Identifiers); any of them. Empty means all.
notCategories Exclude these category ids.
venues, venueSlug Venue slugs (Venue.slug), any of them / one of them.
organizers, organizerSlug Organizer slugs (Organizer.slug).
searchTerm Free text, matched case- and accent-insensitively against titles, descriptions, tags, venue name, organizer names and category labels; when nothing matches literally, titles and descriptions are matched fuzzily (one or two typos).
tag One keyword of Event.tags.
mode online or offline.
superEvent Id of a container event (a festival, a market): its programme.
onlyFeatured Only events the organiser marked as featured.
onlyFeaturedSpecialEvent Only events featured in the licence's special event, where one is configured.
cancelledNotIncluded, soldOutNotIncluded Drop cancelled / sold-out events.
hoursRange HH:mm-HH:mm, e.g. 16:00-22:00: events starting within that window.
groupRepetitionsByDay Expand every future day of a multi-date event into its own node (with that day's startDate), so a listing can show one row per day. Dates on the same day stay in that node's repetitions.
municipality, postalCodes Filter on the venue's address.
sortBy Deprecated: results are always chronological.

Date arguments take YYYY-MM-DD HH:mm:ss followed by an offset (+02:00, +0200, +02 or Z), for example "2026-09-01 00:00:00+02:00". An unparsable date is a Query Arguments invalid error with extensions.invalidArgs naming the argument.

{
  events(
    filter: {
      searchTerm: "konsert"
      fromDate: "2026-09-01 00:00:00+02:00"
      untilDate: "2026-12-31 23:59:59+01:00"
    }
    page: 0
    pageSize: 20
  ) {
    totalCount
    data { id title_nb startDate venue { name } }
  }
}

Other queries

Query Returns
eventByID(eventID: String!) One event by id.
eventBySlug(eventSlug: String!) One event by event_slug (the last segment of eventLink).
eventsBySlugs(eventsSlugs: [String]!) Several events by slug.
eventByTitle(title: String!, lan: String!) One event by exact title; lan is nb or en.
allUpcomingSuperEvents Container events (festivals, markets) that have not ended.
allUpcomingEventsInArea(minLatitude, maxLatitude, minLongitude, maxLongitude) Upcoming events whose venue lies in the box.
categories This licence's categories with ids, labels, slugs and subcategories.
venues, organizers The catalogue of venues and organizers, with ids and slugs.

There are no mutations. Events are published by people through the calendar's own forms and by the calendar's own importers.

Dates

Two facts. Each one has produced a wrong programme on somebody's site.

1. startDate and endDate carry the UTC offset in force on the event's date. Norway is +01:00 in winter and +02:00 in summer, and the value says which:

2026-02-14 19:00:00+01:00    a February concert at 19:00 Oslo time
2026-07-14 19:00:00+02:00    a July concert at 19:00 Oslo time

Both are 19:00 on the wall clock. Both are valid instants. What they are not is "ISO with +00": a parser configured for a fixed offset, or a formatter that prints in the server's own zone, shows 18:00 or 20:00 for one of them and the error flips at every daylight-saving switch. A tourism site painted every hour wrong for weeks this way.

  • The format is YYYY-MM-DD HH:mm:ss±HH:mm with a space between date and time. A strict RFC 3339 parser wants a T: replace the space and it parses everywhere.
  • To display, convert the instant to Europe/Oslo (never to the reader's or the server's zone).
  • Or skip the arithmetic: startTime (HH:mm) is the Norwegian wall-clock start exactly as the organiser typed it, and duration is in minutes. There is no endTime field; derive it from endDate in Europe/Oslo or from startTime + duration.
  • publishingDate and ticketsFromDate follow the same rule. created_at and updated_at are bookkeeping and may not.

2. startDate is the next upcoming occurrence; repetitions lists only future ones. An event with several dates is one event with one id. When its first date has passed, the API promotes the next date that is still current into startDate, endDate, startTime, duration, venue, ticketsURL, eventCancelled and eventSoldOut, and repetitions holds the dates after that one. Past dates are not returned, so the same id answers with a different startDate next week. Do not key your own records on id + startDate unless you want one record per occurrence; if you do, groupRepetitionsByDay gives you the day-nodes directly.

Identifiers

3. Filter and display by id. Names and slugs are presentation.

  • Event.id is the identity of an event for its whole life. event_slug is the URL segment (eventLink is the full URL); title_nb / title_en are edited by people.
  • Event.categories is a list of category ids. Fetch the labels from categories on every run — not once at install time, and never typed by hand. Ids differ between licences (the same label is CONCERT on one calendar and something else on another), and a licence may replace its whole taxonomy: a retired id disappears from categories, the events that carried it are migrated, and the id is never reused for anything else. Filtering on an id that is no longer in categories returns nothing, silently. The changelog records every such change.
  • Venue and Organizer have an id and a slug. The events filter takes the slug (venues, organizers); read it from venues / organizers rather than deriving it from a name.
  • categories returns visible per category; hidden ones are still valid ids on events.

Categories of Tysnes Kalender

As shipped with this build. The categories query is the source of truth at run time.

id name_nb name_en slug_nb slug_en
CONCERT Konsert Concert konsert concert
FESTIVAL Festival Festival festival festival
MUSEUM Galleri / Museum Gallery / Museum galleri-museum gallery-museum
↳ GALLERY Galleri (hidden) Gallery galleri gallery
↳ EXHIBITION Utstilling (hidden) Exhibition utstilling exhibition
↳ MUSEUM Museum (hidden) Museum museum museum
FAMILY Familie Family familie family
THEATER Teater / Show Theater / Show teater-show theater-show
DEBATE Debatt / Samtale Debate / Discussion debatt debate
↳ DEBATE Debatt (hidden) Debate debatt debate
↳ LECTURE Foredrag (hidden) Lecture foredrag lecture
↳ DISCUSSION Samtale (hidden) Discussion samtale discussion
COURSE Kurs Course kurs course
OTHER Annet Other annet other
↳ CONFERENCE Konferanse Conference konferanse conference
↳ DANCE Dans Dance dans dance
↳ FOOD_DRINKS Mat og drikke Food and drinks mat-og-drikke food-and-drinks
↳ GUIDED_TOUR Omvisning Guided Tour omvisning guided-tour
↳ HANDWORK Håndarbeid Handword handarbeid handword
↳ MARKET Marked Market marked market
↳ MOVIES Film Movies film movies
↳ OUTDOORS Friluftsliv Outdoors friluftsliv outdoors
↳ QUIZ Quiz Quiz quiz quiz
↳ SENIOR Senior Senior senior senior
↳ SPORT Idrettsarrangement / E-sport Sports / E-sport idrettsarrangement-e-sport sports-e-sport
↳ TECHNOLOGY Teknologi Technology teknologi technology

Ticket types

Price.type is a ticket-type id. The built-in ones on this calendar:

id name_nb name_en
ASSISTANT Ledsager Assistant
CHILD Barn Child
FAMILY Familie Family
MEMBERS Medlemmer Members
REGULAR Vanlig Regular
REDUCED Redusert Reduced
SENIOR Honnør Senior
STUDENT Student Student

An organiser may also define custom ticket types for their own events; those ids are not listed here, and Price.name_nb / Price.name_en carry the label when the organiser gave one.

Price and capacity

4. Nothing that comes from a form is guaranteed numeric.

  • Price.price is typed Int and the server rounds whatever the organiser stored — but the stored value may be 1.595 (typed with a Norwegian thousands separator), 150,-, or empty. When it cannot be read as a number the field is null. Never divide by it, never assume øre.
  • ticketsInformation says which of free, noTicketsInfo or ticketsInfo applies; prices is only meaningful for ticketsInfo. ticketsURL is where tickets are sold when they are sold elsewhere.
  • duration, minimumAge, maximumAge, cancellationPeriod, views may be null.
  • Capacity fields (registrationEnabled, availableTickets, activeTickets, maxBookingDate, maxBookingTime, paymentMethod) exist for calendars where visitors book through the calendar itself. On a calendar without bookings they are null or false; do not read availableTickets: null as "sold out". eventSoldOut is the organiser's explicit flag.
  • A Repetition may carry its own prices; when it is null, the event's prices apply.

Errors

The response body is always JSON.

Situation HTTP Body
Body is not valid JSON 400 {"errors":[{"message":"Malformed JSON body"}]}
GET without Apollo-Require-Preflight 400 errors[0].extensions.code = "BAD_REQUEST", message mentions CSRF
Query does not validate (unknown field or argument, wrong type) 400 errors[0].extensions.code = "GRAPHQL_VALIDATION_FAILED"; the message names the field
Bad argument value (negative page, unparsable date) 200 data: null, errors[0].extensions.code = "BAD_USER_INPUT" or extensions.invalidArgs
Resolver failure 200 data with null for the failed field and an entry in errors

An errors array can accompany partial data; check for it on every response, not only on non-200 statuses.

extensions.notice is not an error. It is a string on successful responses to requests without a valid key, pointing to this portal. A client that ignores extensions is unaffected; one that reads it can log it once and move on.

Rate limits and quota

None today, keyed or not. When a quota arrives, keyed applications keep their own budget and anonymous traffic first seen after that date may get a lower one. Not yet — this paragraph will change first, and the changelog will say so.

Cache what you can: a listing that changes a few times a day does not need to be fetched every second.

Contact

Questions, a field you need, a change notice you did not get: post@hvaskjerkalender.no. Say which calendar and, if you have one, which key.

Markdown-versjon