integrationen · den enda sida din agent behöver
docsprovisionering · nycklar · kalender · push
Allt en agent behöver för att integrera blint mot hafn. Uppmätt mot servern som kör — inte lovat.
Där verkligheten skaver mot standarden står det utskrivet. En spec som ljuger är värre än ingen.
1 · provisionering (invite-only)
Ett konto med adress, lösenord, kalender och en scopad API-nyckel skapas i ett anrop. Fältnamnen är svenska. Auktoritativt schema: /openapi.json.
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 · ^hafn(-[a-z2-9]{4}){3}$ |
kontonamn | ^[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": "..."}
Lösenordet och API-nyckeln visas en gång och lagras inte hos hafn. Felformen är {"fel", "beskrivning"}: 400 ogiltiga fält · 403 ogiltig eller använd kod · 409 upptaget namn (koden förbrukas inte) · 429 för många försök · 502 bakomliggande fel (inget skapat, koden förbrukas inte).
Samma endpoint tar application/x-www-form-urlencoded och svarar då med en kvittosida i HTML — formulärvägen för människor. Fälten är desamma. Ingen kod? Se /invite.
2 · nyckeln och autentiseringen
Authorization: Bearer API_...
Nyckeln används som Bearer mot JMAP: https://mail.hafn.eu/jmap/. Som lösenord i HTTP Basic avvisas den med 401.
Behörigheterna sitter per JMAP-metod, i camelCase: jmapEmailGet, jmapCalendarEventCreate… Skrivning är delad per operation — jmapEmailCreate/Update/Destroy 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. Nyckeln bär även expiresAt och allowedIps; ett konto kan ha högst fem nycklar, och över taket svarar servern med det vilseledande felet invalidPatch — kontrollera antalet nycklar innan du felsöker något annat.
Rotation utan avbrott: skapa den nya, båda giltiga samtidigt, återkalla den gamla. Kontots lösenord påverkas inte. Men: en återkallad nyckel stänger inte en redan öppen pushström — bryt även uppkopplingen.
3 · kalendersemantik
recurrenceRule är singular och ett objekt. RFC 8984:s pluralform recurrenceRules (array) avvisas med invalidProperties — även i sin enklaste form.
"recurrenceRule": {"frequency": "weekly", "until": "2026-12-31T17:00:00",
"byDay": [{"day": "th"}]}
"recurrenceOverrides": {"2026-09-03T17:00:00": {"start": "2026-09-03T18:30:00"}}
Undantag är recurrenceOverrides, nycklade på förekomstens ursprungliga starttid. Att avsluta en serie är att sätta fältet until — ingen RRULE-sträng behöver någonsin tolkas.
status är förstklassigt: tentative | confirmed | cancelled, och överlever uttaget som STATUS:TENTATIVE.
Heldag: showWithoutTime: true, en duration i dagar, ingen tidszon. Varning: expandRecurrences tappar heldagsflaggan och sätter 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 expansion ärver buggen.
Sök serier som serier: CalendarEvent/query utan expandRecurrences ger mästarnas id. Samtidighet: ifInState (tillståndet är på typnivå). Ägarskap i delat konto: en Participant med calendarAddress — den enda bärare som uppmätt överlever en tredjepartsklient som skriver om händelsen.
4 · uppgifter
Det finns ingen Task/get, och urn:ietf:params:jmap:tasks finns inte i sessionen — leta inte. Uppgifter går genom CalendarEvent-metoderna med "@type": "Task" och progress i stället för status. Båda riktningarna är uppmätta: VTODO via CalDAV läses som Task i JMAP och tvärtom. En dedikerad lista skapas med MKCALENDAR och enbart VTODO i komponentuppsättningen; den syns i Calendar/get.
5 · push och billig ändringskontroll
GET https://mail.hafn.eu/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}
EventSource fungerar. Notisen är en StateChange — datatyp och nytt tillstånd, aldrig vad som ändrats. Prenumerera på types=EmailDelivery,CalendarEvent för nyankommet utan flaggbrus. Servern skickar ingen keepalive (ping= ignoreras) och inga id:-fält — håll egen timer och kör alltid */changes efter varje återuppkoppling.
WebSocket fungerar: wss://mail.hafn.eu/jmap/ws, subprotokoll jmap, push via WebSocketPushEnable.
PushSubscription (webhook) fungerar inte. Prenumerationen lagras men ingen verifiering och ingen notis skickas någonsin — noll 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 öppen ström överlever återkallelse. Rapporterat uppströms; skalet är vägen runt tills det är lagat.
Delta: */changes kostar ~200 byte när inget hänt. Spara tillstånd per typ. Ett skadat sinceState fäller hela begäran med HTTP 400 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 /v1/begar-mer {"konto", "motivering"} → 202; en människa läser och hör av sig. Agentnyckeln har ingen sändning som standard.
7 · mcp-skalet och uttag
För agentklienter finns ett MCP-skal med verktygslista filtrerad per nyckel — hela ytan på /agents. Uttag: ett konto kan ta ut post och kalender i standardformat medan servern kör; hittills provat i liten skala, och det står hellre här än låtsas färdigtestat.
GET hafn.eu/docs.md · text/markdown · 200 · det här är exakt vad din agent hämtar
# 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) - 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