# hafn · docs — integrationen

> Den här sidan räcker för att integrera blint mot hafn. Allt tekniskt nedan är
> uppmätt mot servern som kör (Stalwart 0.16.19 på mail.hafn.eu) — inte läst i
> dokumentation. Auktoritativt schema för provisioneringen: https://hafn.eu/openapi.json
> spec v0.3 · 2026-08-30 · människoform: https://hafn.eu/docs

## 1. Provisionering  (invite-only)

Ett konto med adress, lösenord, kalender och en scopad API-nyckel skapas i ett
anrop. Fältnamnen är svenska.

    POST https://hafn.eu/v1/konto
    Content-Type: application/json

    {"inbjudningskod": "hafn-xxxx-xxxx-xxxx",
     "kontonamn": "exempel",
     "adminkontakt": "namn@exempel.se"}

- inbjudningskod   engångskod från hafn, mönster ^hafn(-[a-z2-9]{4}){3}$
- kontonamn        mönster ^[a-z][a-z0-9-]{2,29}$ — blir adressen kontonamn@hafn.eu
- adminkontakt     e-postadress till kontots kontaktperson

Svar 201:

    {"konto": "...", "adress": "...", "engangslosenord": "...",
     "apinyckel": "API_...", "apinyckelns_scope": ["..."],
     "jmap": "https://mail.hafn.eu/jmap/", "imap": {...}, "smtp": {...},
     "viktigt": "..."}

- engangslosenord och apinyckel visas EN gång och lagras inte hos hafn.
- Fel: 400 ogiltiga fält · 403 ogiltig eller använd kod · 409 upptaget kontonamn
  (koden förbrukas inte) · 429 för många försök · 502 bakomliggande fel (inget
  skapat, koden förbrukas inte). Felform: {"fel": "...", "beskrivning": "..."}.
- Samma endpoint tar application/x-www-form-urlencoded och svarar då med en
  kvittosida i HTML — det är formulärvägen för människor. Fälten är desamma.
- Ingen inbjudningskod? Se https://hafn.eu/invite (hello@hafn.eu).

## 2. Nyckeln och autentiseringen

    Authorization: Bearer API_...

- Nyckeln används som Bearer mot JMAP: https://mail.hafn.eu/jmap/
  Nyckeln som lösenord i HTTP Basic avvisas med 401.
- Behörigheterna sitter per JMAP-metod, i camelCase: jmapEmailGet,
  jmapCalendarEventCreate, jmapPushSubscriptionGet …
  Skrivning är delad per operation: jmapEmailCreate / jmapEmailUpdate /
  jmapEmailDestroy och jmapEmailSubmissionCreate. *Set-namn finns inte.
- Läget Replace ger exakt de listade metoderna, ingenting annat.
- Nyckeln från provisioneringen kan läsa post, läsa och skriva kalender och
  hantera pushprenumerationer. Sändning ingår inte: EmailSubmission/set ger
  forbidden. Scopet står i apinyckelns_scope i svaret.
- Nyckeln bär även expiresAt och allowedIps. Ett konto kan ha högst fem
  API-nycklar; över taket ger servern det vilseledande felet invalidPatch
  "Invalid key for object property" — kontrollera antalet nycklar först.
- Rotation utan avbrott: skapa den nya, båda är giltiga samtidigt, återkalla
  den gamla. Kontots eget lösenord påverkas inte.
- Varning: en återkallad nyckel stänger INTE en redan öppen pushström —
  auktorisationen prövas bara vid uppkopplingen. Bryt även uppkopplingen.

## 3. Kalendersemantik  (uppmätt mot servern)

- Att skapa en händelse KRÄVER calendarIds. Utelämnas fältet avvisas anropet med
  invalidProperties och "Event has to belong to at least one calendar."

      "calendarIds": {"b": true}

  Hitta id:t med Calendar/get — isDefault markerar hushållets huvudkalender.
  En uppgift (@type: Task) läggs i sin egen kalender, t.ex. en VTODO-only-lista.

- recurrenceRule är SINGULAR och ett objekt. RFC 8984:s pluralform
  recurrenceRules (array) avvisas med invalidProperties — även i sin enklaste
  form. Skriv singular:

      "recurrenceRule": {"frequency": "weekly", "until": "2026-12-31T17:00:00",
                         "byDay": [{"day": "th"}]}

- Undantag är recurrenceOverrides — en map nycklad på förekomstens
  URSPRUNGLIGA starttid, med de avvikande fälten som värde:

      "recurrenceOverrides": {"2026-09-03T17:00:00": {"start": "2026-09-03T18:30:00"}}

- Att avsluta en serie är att sätta fältet until. Ingen RRULE-sträng behöver
  någonsin tolkas eller skrivas.
- status är ett förstklassigt fält: tentative | confirmed | cancelled.
  Det överlever uttaget som STATUS:TENTATIVE i iCalendar.
- Heldag: showWithoutTime: true, en duration i dagar, ingen timeZone.
  VARNING: expandRecurrences tappar showWithoutTime och sätter
  timeZone: "Etc/UTC" — även på händelser som inte är återkommande. Läs
  mästarna och expandera själv, eller gå genom MCP-skalet som rättar felet.
  En klient som går rakt på JMAP med expandRecurrences ärver buggen.
- Sök serier som serier: CalendarEvent/query UTAN expandRecurrences
  returnerar mästarnas id, inte förekomsterna.
- Samtidighet: skicka ifInState i CalendarEvent/set. Tillståndet är på
  typnivå (hela kalenderkontot) — en krock kan alltså gälla en annan händelse.
- Ägarskap i ett delat konto: märk personen som Participant med
  calendarAddress (t.ex. mailto:rut@hushall.hafn.eu — adressen behöver ingen
  brevlåda). Det är den enda bärare som uppmätt överlever en tredjepartsklient
  som skriver om händelsen; keywords, categories och color gör det inte.

## 4. Uppgifter

- Det finns ingen Task/get och urn:ietf:params:jmap:tasks finns inte i
  sessionen. Leta inte efter dem.
- Uppgifter går genom CalendarEvent-metoderna med "@type": "Task" i stället
  för "Event", och progress (t.ex. needs-action) i stället för status.
  Båda riktningarna är uppmätta: en VTODO lagd via CalDAV läses som Task i
  JMAP, och en Task skapad via CalendarEvent/set blir korrekt VTODO i CalDAV.
- En dedikerad lista skapas med CalDAV MKCALENDAR och en komponentuppsättning
  med enbart VTODO; den syns i Calendar/get som vilken kalender som helst.

## 5. Push och billig ändringskontroll

- EventSource (SSE) fungerar, med Bearer:

      GET https://mail.hafn.eu/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}

  Notisen är en StateChange: {"@type":"StateChange","changed":{"<konto>":
  {"CalendarEvent":"<state>"}}} — datatyp och nytt tillstånd, ALDRIG vad som
  ändrats. Prenumerera på types=EmailDelivery,CalendarEvent för nyankommen
  post utan flaggbrus.
- Servern skickar ingen keepalive (ping= ignoreras) och inga id:-fält, så
  Last-Event-ID-återupptagning finns inte. Håll egen timer, och kör alltid
  */changes efter varje återuppkoppling.
- WebSocket fungerar: wss://mail.hafn.eu/jmap/ws, subprotokoll jmap, push slås
  på med {"@type":"WebSocketPushEnable","dataTypes":[...]}.
- PushSubscription (webhook) fungerar INTE: prenumerationen lagras men ingen
  verifiering och ingen notis skickas någonsin (0 utgående paket, uppmätt).
  Bygg ingenting på den.
- Känd lucka: strömmarna lyder inte nyckelns scope — en nyckel utan läsrätt
  får ändå varje StateChange, och en återkallad nyckel behåller en öppen
  ström. Rapporterat uppströms; MCP-skalet är vägen runt tills det är lagat.
- Delta: */changes är billigt (~200 B när inget hänt). Spara tillstånd PER TYP.
  Ett skadat sinceState fäller hela begäran med HTTP 400
  urn:ietf:params:jmap:error:notRequest — tolka det som "gör full synk".
  sessionState duger inte som ändringssignal.

## 6. Gränser

- Utgående post: 100 brev per dygn. Taket räknas i dag per avsändaradress och
  på könivå: brev 101 avvisas inte utan skjuts upp till nästa UTC-dygn.
  Högre tak: POST https://hafn.eu/v1/begar-mer {"konto": "...", "motivering": "..."}
  → 202; en människa läser förfrågan och hör av sig. Eller hello@hafn.eu.
- Agentnyckeln har ingen sändning som standard. Sändning är en egen behörighet
  (jmapEmailSubmissionCreate) som kontots ägare måste ge uttryckligen.

## 7. MCP-skalet

För agentklienter finns ett MCP-skal (streamable HTTP) över samma data, med
verktygslista filtrerad per nyckelns scope. Hela ytan: https://hafn.eu/agents.md

## 8. Uttag

Ett konto kan ta ut post och kalender i standardformat medan servern kör —
uttaget stoppar ingen annan. Hittills provat i liten skala; formaten är
vanlig mbox/ICS-standard som andra system läser.

## Källa och sanning

Fältnamnen i §1 är verifierade mot https://hafn.eu/openapi.json och
https://hafn.eu/llms.txt. Ändras något av detta ändras dokumentet först.
Frågor: hello@hafn.eu
