# Tysnes Kalender — developer portal

Tysnes Kalender publishes its events through a **public GraphQL API**. No account, no key and no
approval are needed to read it: the data is public and stays public.

- Endpoint: `https://hvaskjer-staging.web.app/graphQL`
- Documentation: [API](/utviklere/api) · [Reference](/utviklere/reference) · [Widget](/utviklere/widget) · [Changelog](/utviklere/changelog) · [Keys](/utviklere/keys)
- Machine-readable copies: [llms.txt](https://hvaskjer-staging.web.app/utviklere/llms.txt), [schema.graphql](https://hvaskjer-staging.web.app/utviklere/schema.graphql), and every page as markdown at `https://hvaskjer-staging.web.app/utviklere/<page>.md` (or send `Accept: text/markdown`).

The API is **per licence**: this endpoint only ever answers with this calendar's events. Another
kommune's calendar has its own endpoint, its own category ids and its own keys.

## The three rules

Every one of these has cost somebody weeks. Read them before the first query.

1. **IDs are the contract.** Filter and match on `id` (events, venues, organizers) and on category ids.
   Names and slugs are presentation: they are edited by people, they differ between licences, and
   they change. Fetch the labels from `categories` on every run; never cache them or write them by hand.
2. **Dates carry the UTC offset in force on the event's date.** `startDate` and `endDate` look like
   `2026-11-01 10:00:00+01:00` in winter and `2026-07-01 10:00:00+02:00` in summer. They are not
   "ISO with +00", and the separator is a space, not a `T`. Render them in `Europe/Oslo`, or use
   `startTime` (the Norwegian wall clock as the organiser typed it) and `duration` (minutes).
3. **Nothing that comes from a form is guaranteed numeric.** `Price.price` is typed `Int` and is
   rounded at the boundary, but the organiser may have typed `1.595` or `150,-`; `duration`,
   `availableTickets`, `minimumAge` may be null. Treat every numeric field defensively.

## A first call

```bash
curl -s https://hvaskjer-staging.web.app/graphQL \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ events(page: 0, pageSize: 3) { totalCount hasMore data { id title_nb startDate startTime venue { name } eventLink } } }"}'
```

The same query, readable:

```graphql
{
  events(page: 0, pageSize: 3) {
    totalCount
    hasMore
    data {
      id
      title_nb
      startDate
      startTime
      venue { name }
      eventLink
    }
  }
}
```

`startDate` is the **next upcoming occurrence** of the event and `repetitions` lists only the
future ones; past dates are never returned. `page` starts at 0 and `pageSize` defaults to 10.
The [API page](/utviklere/api) has the filter, the date rules and the errors.

## Identify yourself

Send an API key with every request:

```
X-Api-Key: hsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

The key is **identity, not security** — the data is the same with or without it. What it buys you:

- **Change notices.** A key carries a contact address. When a change would affect your queries
  (a category retired, a field renamed, a licence moved), the contacts of the keys that use
  those fields are told before it happens. Anonymous callers find out when something breaks.
- **Your own record.** Anonymous requests are grouped by the shape of the query; with a key,
  yours are yours, which matters the day you ask why something looks wrong.
- **Your own quota, later.** There is no rate limit today. When one arrives, keyed traffic keeps
  its own budget.

One key per application; a consumer reading five calendars asks each of them for a key.
Create yours on the [Keys page](/utviklere/keys) — you need an account on Tysnes Kalender with a
verified e-mail address. The same page gives you a block of instructions to paste to a coding agent.

Responses to anonymous requests carry an `extensions.notice` string saying as much. It is
informational; a client that never looks at `extensions` is not affected.
