# KiiOn Developer Docs — complete KiiOn är en plattform för access och upplåsning. Via REST-API:t låser ert system upp dörrar och enheter online, medan era egna regler avgör vem som får öppna vad och när. Tre anrop räcker: logga in, hämta enhetslistan, lås upp. - Bas-URL STAGE: https://staging.zesec.com/api - Bas-URL PROD: https://api.zesec.com/api - Autentisering: `Authorization: Bearer ` (JWT). - Refresh-tokens är engångs och roterande — spara både ny access- och ny refresh-token atomiskt. - En aktiv session per konto: logga in server-side, aldrig vid varje upplåsning. - Unlock är `PUT /api/device/{deviceId}/unlock` och får aldrig retry:as automatiskt. Source: https://developers.kiion.io. OpenAPI: https://developers.kiion.io/openapi.json --- # Introduktion KiiOn är en plattform för access och upplåsning. Via API:t låser ert system upp dörrar och enheter online — styrt av era egna regler för vem som får öppna vad och när. ## Vad är KiiOn? KiiOn (byggt på Zesec-plattformen) styr fysisk passage. Enheterna sitter i dörrar och är uppkopplade — i första hand via LTE, med WiFi som backup. Ert anrop till API:t skickar ett upplåsningskommando till rätt enhet, och enheten öppnar dörren. API:t är ett vanligt REST-API över HTTPS med JWT-baserad autentisering. Ni behöver ingen särskild hårdvara eller SDK för att komma igång — tre anrop räcker för att låsa upp en dörr. ## Vad kan du bygga? Det vanligaste upplägget är att integratören bygger upplåsningen direkt i sin egen produkt: era användare öppnar i **er** app, ni avgör i **ert** system vem som får öppna vilken dörr och när, och API-kontot har den tekniska accessen att utföra upplåsningen. Reglerna och gränssnittet äger ni. - Ett incheckningsflöde där appen ger gästen en tidsbegränsad öppningsknapp. - Ett QR-flöde där en skannad kod validerar mot ert backend, som i sin tur anropar unlock. - En bokningstjänst som öppnar rätt lokal under bokad tid. - Ett fastighetssystem som ger servicepersonal access under ett arbetsorderfönster. > **KiiOn utför, ni bestämmer** — KiiOn tar aldrig ställning till om just den här användaren borde få öppna. Behörighetslogiken ligger hos er; API:t utför upplåsningen när ni ber om det. ## Vad krävs? - **Ett API-konto** — tilldelas och administreras av KiiOn. Se [Säkerhetsmodell](/docs/security-model) för varför, och [Få access](/docs/access) för hur du begär ett. - **STAGE-miljön** för utveckling och test: `https://staging.zesec.com`. - **PROD-miljön** för skarp drift: `https://api.zesec.com` — när ni blivit verifierad partner. Se [Från sandbox till produktion](/docs/production). ## Nästa steg - [Säkerhetsmodell — varför KiiOn tilldelar access](/docs/security-model) - [Quickstart — din första upplåsning på tre anrop](/docs/quickstart) - [Koncept & entitetsmodell — Företag → Plats → Dörr → Nyckel](/docs/concepts) --- # Säkerhetsmodell Accesskontroll tål ingen otydlighet. Här är hur behörigheter tilldelas, vad som är öppet och vad som skyddas. ## Access tilldelas av KiiOn Av säkerhetsskäl kan en integratör inte själv skapa eller tilldela API-access. All access tilldelas och administreras av KiiOn. Det är den enda garantin för att ingen kan ge sig själv åtkomst till någon annans enheter — och den garantin är hela poängen med ett passersystem. Rent praktiskt: KiiOn skapar API-kontot, kopplar det till rätt företag och delar de nycklar kontot ska ha. Se [Få access](/docs/access) för vad ni skickar in och vad ni får tillbaka. ## Dokumentationen är öppen Den här dokumentationen och hela API-referensen är publika. Du behöver inget avtal för att läsa, förstå eller testa integrationen i sandbox. Det vi skyddar är inloggningsuppgifter, åtkomst till riktiga enheter och persondata — inte kunskapen om hur API:t fungerar. > **Öppenhet är inte en säkerhetsrisk** — Ett passersystem vars säkerhet vilar på att API:t är hemligt är inte säkert. Vår säkerhet ligger i tilldelningen av access och i tokenhanteringen — inte i att dölja endpoints. ## Avtal: rätt sak på rätt nivå Avtal hör hemma där riktiga enheter och persondata är inblandade — inte framför dokumentationen. Nivåerna beskrivs i sin helhet på [Från sandbox till produktion](/docs/production). - **Publikt** — den här dokumentationen och API-referensen. Inget avtal. - **Sandbox (STAGE)** — ett lättviktigt click-through-utvecklaravtal. Ingen manuell NDA. Se [Utvecklaravtal](/legal/terms). - **Produktion (PROD)** — här tecknas ömsesidig NDA och ett databehandlaravtal ([DPA](/legal/dpa)), eftersom persondata då hanteras. Det är branschstandard och gäller PROD-access, inte dokumentationen. ## En aktiv session per konto Ett API-konto kan bara ha en aktiv session. En ny inloggning på samma konto avlivar den föregående. Logga därför in server-side och återanvänd token i stället för att logga in vid varje anrop — se [Autentisering & tokens](/docs/authentication). ## Rapportera sårbarheter Hittar du ett säkerhetsproblem vill vi veta det. Se [Ansvarsfull rapportering](/docs/disclosure) för kanal, svarstider och vad som ingår i scope — testning sker mot STAGE, aldrig mot riktiga installerade enheter. --- # Koncept & entitetsmodell För att förstå hur access är uppbyggd i KiiOn behöver du känna till fyra begrepp och en regel om deviceId. ## Hierarkin | Nivå | Vad det är | Exempel | | --- | --- | --- | | Företag | Organisationen som äger platserna | Example Inc. | | Plats | En adress eller anläggning under företaget | Location 1 | | Dörr | En fysisk dörr med en uppkopplad enhet | Door A | | Nyckel | Rätten att öppna en eller flera dörrar | Nyckel till Door A | Ett API-konto är ett konto i den här strukturen. Det kan **äga** enheter (dörrarna ligger under ert eget företag) eller ha fått **nyckeldelad** access till andra företags dörrar — eller båda delarna. ## Hur access delas - **Egna enheter:** era framtida kunder skapas som plats/dörr under ert företag. Ni administrerar strukturen. - **Nyckeldelning:** dörren ligger kvar hos kunden, och en nyckel delas till ert API-konto. Kunden behåller ägarskapet. Vilket upplägg som passar beror på affären. KiiOn sätter upp rätt struktur när integrationen aktiveras — ta med det i beskrivningen när ni [begär access](/docs/access). ## deviceId är nyckeln till upplåsning Själva unlock-anropet identifierar dörren med ett `{deviceId}`. Det är ett heltal, och det hämtas ur enhetslistan — inte ur en konfigurationsfil. > **Hårdkoda inte deviceId** — Hämta `GET /api/user/device`, mappa era interna rum/dörrar till KiiOns id och cachelagra mappningen. Ett hårdkodat id slutar fungera så fort en enhet byts ut. Det är fältet `id` i varje objekt i svaret från `GET /api/user/device` som fyller `{deviceId}` i `PUT /api/device/{deviceId}/unlock`. Fälten `name` och `locationName` är de ni mappar mot era egna dörrnamn. **GET /api/user/device** ```json [ { "id": 1001, "name": "Door A", "locationId": 42, "locationName": "Location 1", "companyName": "Example Inc.", "doorType": "Entrance", "online": true }, { "id": 1002, "name": "Door B", "locationId": 42, "locationName": "Location 1", "companyName": "Example Inc.", "doorType": "Garage", "online": true } ] ``` **Vidare** - [Quickstart — hämta listan och lås upp](/docs/quickstart) - [Golden path — var mappningen ska cachelagras](/docs/golden-path) --- # Quickstart – din första upplåsning Målet: låsa upp en testenhet i STAGE med tre anrop. Bas-URL (STAGE): `https://staging.zesec.com/api` ## Förutsättningar - Ett STAGE-API-konto (`{{phoneNumber}}` + `{{password}}`). Har du inget? Se [Få access](/docs/access) och [Från sandbox till produktion](/docs/production). - En testenhet som ert konto har nyckel till. - cURL, Swagger-UI eller Postman. > **En aktiv session per konto** — En ny inloggning på samma konto avlivar den föregående sessionen. Testa inte från två klienter samtidigt, och logga in server-side i den riktiga integrationen — annars slår er egen testning ut er produktionssession. ## 1. Logga in `POST /api/auth/login-by-phone` med telefonnummer och lösenord. Du får en access-token (JWT) och en refresh-token. **Begäran** _cURL_ ```bash curl -X POST https://staging.zesec.com/api/auth/login-by-phone \ -H "Content-Type: application/json" \ -d '{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}' ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/auth/login-by-phone`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phoneNumber: PHONE_NUMBER, password: PASSWORD }), }); if (!res.ok) throw new Error(`login failed: ${res.status}`); const { AccessToken, RefreshToken } = await res.json(); ``` _Python_ ```python import requests res = requests.post( f"{BASE_URL}/auth/login-by-phone", json={"phoneNumber": PHONE_NUMBER, "password": PASSWORD}, timeout=10, ) res.raise_for_status() tokens = res.json() # {"AccessToken": "...", "RefreshToken": "..."} ``` **Svar** ```json { "AccessToken": "", "RefreshToken": "" } ``` Spara båda. Access-token används i `Authorization`-headern; refresh-token används för att förnya paret — se [Autentisering & tokens](/docs/authentication). ## 2. Hämta dina enheter `GET /api/user/device` returnerar de enheter kontot har access till. Fältet `id` är det `{deviceId}` du behöver i steg 3. **Begäran** _cURL_ ```bash curl https://staging.zesec.com/api/user/device \ -H "Authorization: Bearer " ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/user/device`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const devices = await res.json(); // Persist the mapping: your internal door key -> device.id const mapping = new Map(devices.map((d) => [d.name, d.id])); ``` _Python_ ```python res = requests.get( f"{BASE_URL}/user/device", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) res.raise_for_status() mapping = {d["name"]: d["id"] for d in res.json()} ``` **Svar** ```json [ { "id": 1001, "name": "Door A", "locationId": 42, "locationName": "Location 1", "companyName": "Example Inc.", "doorType": "Entrance", "online": true }, { "id": 1002, "name": "Door B", "locationId": 42, "locationName": "Location 1", "companyName": "Example Inc.", "doorType": "Garage", "online": true } ] ``` > **Mappa, cachelagra, hårdkoda inte** — Matcha `name`/`locationName` mot era egna dörrnamn en gång och spara mappningen. Se [Koncept & entitetsmodell](/docs/concepts#device-id). ## 3. Lås upp `PUT /api/device/{deviceId}/unlock` med access-token i headern. Enheten tar emot kommandot och öppnar dörren. **Begäran** _cURL_ ```bash curl -X PUT https://staging.zesec.com/api/device/{deviceId}/unlock \ -H "Authorization: Bearer " ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/device/${deviceId}/unlock`, { method: "PUT", headers: { Authorization: `Bearer ${accessToken}` }, }); // 200 means the unlock command was accepted and forwarded to the device. if (!res.ok) throw new UnlockFailed(res.status); ``` _Python_ ```python res = requests.put( f"{BASE_URL}/device/{device_id}/unlock", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) res.raise_for_status() # do not blind-retry: see the limits page ``` **Svar** ```text 200 OK — kommandot är accepterat och vidarebefordrat till enheten. ``` > **200 betyder accepterat, inte bekräftat öppnad** — Svaret säger att KiiOn tog emot och skickade kommandot. Vill ni verifiera tillståndet, läs enheten med `GET /api/device/{deviceId}`. Gör aldrig blind retry på unlock — se [Gränser & retry](/docs/limits). **Vanliga fel i steg 3** | Status | Betyder | Gör så här | | --- | --- | --- | | 401 | Access-token saknas eller har gått ut | Förnya via `/api/auth/refresh` och gör om anropet | | 403 | Kontot har ingen nyckel till enheten | Kontrollera nyckeldelningen med KiiOn | | 404 | Okänt `{deviceId}` | Hämta enhetslistan på nytt — id:t kan ha ändrats | | 5xx | Enheten eller gatewayen svarar inte | Visa felet för användaren, retry:a inte i loop | ## Samma flöde i Swagger 1. Öppna [API-referensen](/docs/api-reference) — den renderas direkt ur `/openapi.json`. 2. Välj servern **STAGE** i serverväljaren. 3. Kör `POST /api/auth/login-by-phone` med `{{phoneNumber}}` och `{{password}}`. Kopiera `AccessToken` ur svaret. 4. Klicka **Authorize** och klistra in `Bearer `. 5. Kör `GET /api/user/device` och notera `id` för testdörren. 6. Kör `PUT /api/device/{deviceId}/unlock` med det id:t. ## Samma flöde i Postman Ladda ner [KiiOn-samlingen för Postman](/kiion-postman-collection.json) och importera den (**Import → File**). Samlingen innehåller de fyra anropen och en miljö med variablerna nedan. 1. Sätt `BASE_URL` till `https://staging.zesec.com/api`. 2. Sätt `phoneNumber` och `password` till era STAGE-uppgifter. 3. Kör **1. Login by phone** — testskriptet sparar `AccessToken` och `RefreshToken` som samlingsvariabler automatiskt. 4. Kör **2. List devices** och sätt `deviceId` till `id` för testdörren. 5. Kör **3. Unlock device**. > **Spara aldrig riktiga uppgifter i samlingen** — Lägg `phoneNumber` och `password` i en Postman-miljö av typen *initial value: tom*, så att de inte följer med när samlingen exporteras eller delas. ## Nästa steg - [Autentisering & tokens — proaktiv förnyelse och 401-fallback](/docs/authentication) - [Golden path — så bygger ni integrationen robust](/docs/golden-path) - [Hela API-referensen](/docs/api-reference) --- # Autentisering & tokens Den här sidan avgör om integrationen blir stabil. Läs den innan ni skriver den första raden klientkod. ## Inloggning Logga in med telefonnummer och lösenord via `POST /api/auth/login-by-phone`. Svaret innehåller en access-token (en JWT) och en refresh-token. **Begäran** _cURL_ ```bash curl -X POST https://staging.zesec.com/api/auth/login-by-phone \ -H "Content-Type: application/json" \ -d '{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}' ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/auth/login-by-phone`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phoneNumber: PHONE_NUMBER, password: PASSWORD }), }); if (!res.ok) throw new Error(`login failed: ${res.status}`); const { AccessToken, RefreshToken } = await res.json(); ``` _Python_ ```python import requests res = requests.post( f"{BASE_URL}/auth/login-by-phone", json={"phoneNumber": PHONE_NUMBER, "password": PASSWORD}, timeout=10, ) res.raise_for_status() tokens = res.json() # {"AccessToken": "...", "RefreshToken": "..."} ``` **Svar** ```json { "AccessToken": "", "RefreshToken": "" } ``` > **Fältnamnens skiftläge** — Begäran följer OpenAPI-kontraktet och använder `phoneNumber`, `password` och `refreshToken`. Svarets tokenfält dokumenteras som `AccessToken` och `RefreshToken`. Läs svaret skiftlägestolerant om ni vill vara helt robusta. ## Livslängder | Token | Livslängd | Egenskap | | --- | --- | --- | | Access-token (JWT) | ~2 dagar (48 h) | Skickas som `Authorization: Bearer `. Exakt utgång står i `exp` i JWT:n — den är auktoritativ. | | Refresh-token | upp till 30 dagar | **Engångs och roterande.** En refresh returnerar både ny access- och ny refresh-token, och den gamla dör omedelbart. | Planera för proaktiv förnyelse **var 36:e timme**, eller läs `exp` ur access-token och förnya några timmar innan den går ut. Det är samma intervall i hela portalen — hittar ni en annan siffra någonstans är det ett fel, rapportera det. ## Token-refresh `POST /api/auth/refresh` med den nuvarande refresh-token i bodyn. Ingen `Authorization`-header behövs. **Begäran** ```bash curl -X POST https://staging.zesec.com/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken": ""}' ``` **Svar** ```json { "AccessToken": "", "RefreshToken": "" } ``` > **Den gamla refresh-token är död i samma ögonblick** — Så fort svaret är skickat är den token ni skickade in ogiltig. Att spara den nya access-token men glömma den nya refresh-token är det vanligaste felet i den här integrationen — nästa förnyelse misslyckas och kontot måste logga in på nytt. Se [Felsökning & FAQ](/docs/troubleshooting#lost-refresh-token). **Spara båda i en skrivning** ```typescript async function rotate(currentRefreshToken: string) { const res = await fetch(`${BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: currentRefreshToken }), }); if (!res.ok) throw new RefreshFailed(res.status); const { AccessToken, RefreshToken } = await res.json(); // One write. Never persist the access token without the refresh token that came with it: // the token you sent is already dead, so a partial save loses the session for good. await session.replace({ accessToken: AccessToken, refreshToken: RefreshToken, rotatedAt: Date.now() }); return AccessToken; } ``` ## Var tokens ska lagras - Server-side, i er backend. Aldrig i en webbklient, mobilapp eller i frontend-kod. - En delad lagring per API-konto (databas, Redis eller motsvarande) så att alla instanser ser samma par. - Skriv paret atomiskt. En halv skrivning förlorar sessionen permanent. - Logga aldrig tokenvärden. Logga rotationstidpunkter, inte innehåll. ## Beslutstabell vid fel Det finns exakt två felvägar att implementera, och de ska hanteras olika. | Läge | Vad hände | Gör så här | | --- | --- | --- | | Anrop svarar `401` | Access-token har gått ut eller är ogiltig | Kör en refresh. Lyckas den: gör om det ursprungliga anropet en gång. | | Refresh svarar `2xx` | Nytt par utfärdat | Spara båda tokens atomiskt och fortsätt. | | Refresh svarar `401`/`400` | Refresh-token är förbrukad eller utgången | Full ominloggning med `{{phoneNumber}}`/`{{password}}`. Retry:a **inte** samma refresh-token. | | Ominloggning misslyckas | Uppgifterna är fel, kontot spärrat eller API:t nere | Sluta försöka. Larma en operatör. En loop här låser ute kontot. | | Anrop svarar `401` igen efter refresh | Sessionen togs över av en annan inloggning | Sluta försöka. Se [En aktiv session per konto](#one-session-per-account). | | Nätverksfel / `5xx` på refresh | Tillfälligt fel — token kan vara förbrukad eller inte | Backa av och försök **en** gång. Fortsätter det: ominloggning. | **Komplett tokenhanterare** ```typescript const PROACTIVE_REFRESH_MS = 36 * 60 * 60 * 1000; let inFlight: Promise | null = null; /** Single active session per account: one process, one token pair, one refresh at a time. */ export async function getValidAccessToken(): Promise { const current = await session.read(); if (current && Date.now() - current.rotatedAt < PROACTIVE_REFRESH_MS) { return current.accessToken; } // Single-flight guard: concurrent callers must not each burn a single-use refresh token. inFlight ??= (async () => { try { return current ? await rotate(current.refreshToken) : await login(); } catch (err) { if (err instanceof RefreshFailed) return await login(); // refresh token spent or expired throw err; } finally { inFlight = null; } })(); return inFlight; } /** Wrap every API call: one refresh, one replay, then surface the failure. */ export async function apiCall(path: string, init: RequestInit = {}) { let token = await getValidAccessToken(); let res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); if (res.status === 401) { await session.invalidateAccessToken(); token = await getValidAccessToken(); res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); } return res; // still 401 -> stop and alert an operator; do not loop } ``` > **Single-flight är inte valfritt** — Två samtidiga 401:or utan lås ger två parallella refresh-anrop. Det andra använder en token som det första just förbrukade, och sessionen dör. Guarden `inFlight` i exemplet ovan är det som förhindrar det. ## En aktiv session per konto Ett API-konto har en session. En ny inloggning på samma konto avlivar den föregående. Kör därför inte flera klienter som loggar in med samma konto — låt backend vara den enda som loggar in, och avsluta sessioner ni är klara med via `POST /api/auth/logout`. > **Detta drabbar er egen testning** — Ett lokalt testskript som loggar in med produktionskontot slår ut produktionssessionen. Använd separata konton per miljö. ## Checklista - [ ] Inloggning sker server-side, inte vid varje öppning - [ ] Proaktiv förnyelse var 36:e timme eller utifrån `exp` - [ ] Både access- och refresh-token sparas i samma atomiska skrivning - [ ] 401 → refresh → gör om anropet **en** gång - [ ] Misslyckad refresh → ominloggning, aldrig retry på samma token - [ ] Single-flight-lås runt förnyelsen - [ ] Tokens lagras server-side och loggas aldrig **Vidare** - [Statuskoder & felsvar — hela katalogen](/docs/errors) - [Gränser & retry — vad som får försökas om](/docs/limits) - [Felsökning & FAQ](/docs/troubleshooting) --- # API-referens Referensen nedan renderas direkt ur `/openapi.json` — den är genererad, inte handskriven, och kan därför inte glida ifrån API:t. ## Bas-URL:er | Miljö | Bas-URL | Används till | | --- | --- | --- | | STAGE | `https://staging.zesec.com/api` | Utveckling och test. Alla får access hit. | | PROD | `https://api.zesec.com/api` | Skarp drift mot riktiga enheter. Kräver verifierat partnerskap. | Sökvägarna är identiska i båda miljöerna. Serverväljaren i referensen nedan byter mellan dem. ## De fyra centrala endpointerna | Metod & sökväg | Auth | Body / parameter | Ger | | --- | --- | --- | --- | | `POST /api/auth/login-by-phone` | — | `{ "phoneNumber": "{{phoneNumber}}", "password": "{{password}}" }` | `AccessToken` + `RefreshToken` | | `GET /api/user/device` | `Bearer ` | — | Enhetslista för mappning och cache | | `PUT /api/device/{deviceId}/unlock` | `Bearer ` | `{deviceId}` i sökvägen (heltal) | Utför upplåsningen | | `POST /api/auth/refresh` | — | `{ "refreshToken": "" }` | Nytt, roterat tokenpar | Utöver dessa publicerar referensen `POST /api/auth/login`, `POST /api/auth/login-by-email`, `POST /api/auth/logout`, `GET /api/user/location`, `GET /api/device/{deviceId}` och `PUT /api/device/{deviceId}/unlock/{lockId}`. Allt annat i den interna specen filtreras bort innan dokumentet serveras. > **lockId behövs bara för enheter med flera lås** — `PUT /api/device/{deviceId}/unlock/{lockId}` riktar upplåsningen mot ett specifikt lås på en enhet som har fler än ett. Har er enhet ett lås använder ni endpointen utan `{lockId}`. > **Interaktiv referens** — Referensen renderas i webbläsaren. Läser du det här som text: hämta dokumentet direkt från /openapi.json (eller /openapi.yaml). ## Maskinläsbara varianter - [/openapi.json — det serverade dokumentet (live när källan svarar)](/openapi.json) - [/openapi.yaml — samma dokument som YAML](/openapi.yaml) - [/openapi.pinned.json — den fastlåsta kopian, ändras aldrig oväntat](/openapi.pinned.json) - [/llms.txt — index för AI-agenter](/llms.txt) Svaret från `/openapi.json` bär headern `X-Spec-Source: live` eller `pinned` så att ni kan se vilket dokument ni fick, plus en `ETag` för villkorlig hämtning. ## Fel och statuskoder Varje skyddad operation dokumenterar sina 4xx-svar. Hela katalogen med vad klienten ska göra i varje läge finns på [Statuskoder & felsvar](/docs/errors). --- # Golden path & rekommenderad arkitektur Följer ni det här mönstret blir integrationen både snabbare och stabilare — och ni behöver nästan aldrig kontakta supporten. ## Sekvensen 1. **Vid uppstart / på schema:** logga in server-side en gång. Spara access- och refresh-token i delad lagring. 2. **På schema:** förnya token proaktivt var 36:e timme, inte när något gått sönder. 3. **På schema eller vid behov:** hämta enhetslistan och skriv om mappningen. Ett dygn (24 h) är en rimlig utgångspunkt. 4. **I öppningsögonblicket:** slå upp `{deviceId}` i mappningen och gör **ett** unlock-anrop. > **Antimönster: logga in vid varje öppning** — Det är det enskilt vanligaste felet. Varje inloggning avlivar den föregående sessionen, ger latens i öppningsögonblicket och gör att två parallella öppningar slår ut varandra. Logga in server-side, en gång. ## Ansvarsfördelningen | Lager | Ansvar | Får inte | | --- | --- | --- | | Er klient / app | Visa dörrar, ta emot ett tryck | Aldrig hålla tokens eller anropa KiiOn direkt | | Er backend | Behörighetsregler, tokenhantering, mappning, audit | Logga in per öppning | | KiiOn-API:t | Utföra upplåsningen | Avgöra om användaren borde få öppna | ## 1. Tokenlagret En singleton per API-konto som äger inloggning, förnyelse och 401-hantering. All annan kod ber om en giltig token, aldrig om en inloggning. Detaljerna finns i [Autentisering & tokens](/docs/authentication#failure-handling). ```typescript const PROACTIVE_REFRESH_MS = 36 * 60 * 60 * 1000; let inFlight: Promise | null = null; /** Single active session per account: one process, one token pair, one refresh at a time. */ export async function getValidAccessToken(): Promise { const current = await session.read(); if (current && Date.now() - current.rotatedAt < PROACTIVE_REFRESH_MS) { return current.accessToken; } // Single-flight guard: concurrent callers must not each burn a single-use refresh token. inFlight ??= (async () => { try { return current ? await rotate(current.refreshToken) : await login(); } catch (err) { if (err instanceof RefreshFailed) return await login(); // refresh token spent or expired throw err; } finally { inFlight = null; } })(); return inFlight; } /** Wrap every API call: one refresh, one replay, then surface the failure. */ export async function apiCall(path: string, init: RequestInit = {}) { let token = await getValidAccessToken(); let res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); if (res.status === 401) { await session.invalidateAccessToken(); token = await getValidAccessToken(); res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); } return res; // still 401 -> stop and alert an operator; do not loop } ``` ## 2. Enhetsmappningen Enhetslistan ändras sällan; unlock-anropen sker ofta. Hämta listan på schema och spara minst `Device.Id` och `Device.Name` — gärna även plats och företag så att felsökning går att göra utan att slå i API:t. ```typescript /** Runs on a schedule, not on the unlock path. */ export async function syncDevices() { const res = await apiCall("/user/device"); const devices: Device[] = await res.json(); await db.deviceMapping.replaceAll( devices.map((d) => ({ kiionDeviceId: d.id, // fills {deviceId} in the unlock call kiionName: d.name, locationId: d.locationId, locationName: d.locationName, syncedAt: Date.now(), })), ); } ``` - Uppdatera mappningen var 24:e timme, samt när ni får `404` på ett känt `{deviceId}`. - Låt mappningen vara nyckeln mellan er interna dörridentitet och KiiOns `id` — hårdkoda aldrig id:t. - Spara `syncedAt` så att ni kan se hur gammal mappningen är när något ser fel ut. ## 3. Öppningsögonblicket Här ska det bara finnas ett HTTP-anrop. Behörighetskontrollen är er, uppslaget är lokalt, token är redan giltig. ```typescript /** Everything expensive already happened. This is one call. */ export async function openDoor(internalDoorId: string, actor: User) { // 1. YOUR rules decide. KiiOn does not. if (!(await mayOpen(actor, internalDoorId))) throw new Forbidden(); // 2. Cached mapping — no device-list call here. const deviceId = await db.deviceMapping.kiionIdFor(internalDoorId); if (!deviceId) throw new NotMapped(internalDoorId); // 3. One call, with a token that is already valid. const res = await apiCall(`/device/${deviceId}/unlock`, { method: "PUT" }); await audit.record({ actor, internalDoorId, deviceId, status: res.status }); return res.ok; } ``` > **Retry:a inte unlock blint** — Ett upprepat anrop kan öppna dörren en gång till. Visa felet i stället, och läs [Gränser & retry](/docs/limits#unlock-retry) innan ni bygger någon form av automatik. ## Checklista - [ ] Inloggning sker server-side och återanvänds - [ ] Proaktiv token-förnyelse med single-flight-lås - [ ] 401-fallback som gör om anropet en gång - [ ] Roterad refresh-token sparas atomiskt - [ ] Enhetslistan cachelagras och hårdkodas inte - [ ] Öppningsögonblicket innehåller exakt ett API-anrop - [ ] Varje upplåsning skrivs till er egen audit-logg **Vidare** - [Kodexempel — de fyra funktionerna i cURL, Node och Python](/docs/code-examples) - [Från sandbox till produktion — checklistan före PROD](/docs/production) --- # Kodexempel Fyra funktioner räcker för en komplett integration. Alla värden nedan är platshållare — byt ut dem mot era egna via miljövariabler, aldrig i koden. **Konfiguration** ```bash BASE_URL=https://staging.zesec.com/api KIION_PHONE_NUMBER={{phoneNumber}} KIION_PASSWORD={{password}} ``` > **Inga uppgifter i kod** — `{{phoneNumber}}` och `{{password}}` hör hemma i er hemlighetshantering. Checka aldrig in dem, och logga aldrig tokens. ## 1. login Körs en gång vid uppstart, och som fallback när en refresh misslyckats. Aldrig per öppning. _cURL_ ```bash curl -X POST https://staging.zesec.com/api/auth/login-by-phone \ -H "Content-Type: application/json" \ -d '{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}' ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/auth/login-by-phone`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phoneNumber: PHONE_NUMBER, password: PASSWORD }), }); if (!res.ok) throw new Error(`login failed: ${res.status}`); const { AccessToken, RefreshToken } = await res.json(); ``` _Python_ ```python import requests res = requests.post( f"{BASE_URL}/auth/login-by-phone", json={"phoneNumber": PHONE_NUMBER, "password": PASSWORD}, timeout=10, ) res.raise_for_status() tokens = res.json() # {"AccessToken": "...", "RefreshToken": "..."} ``` ## 2. getValidAccessToken Proaktiv förnyelse var 36:e timme, single-flight-lås och fallback till ominloggning. All annan kod anropar den här, aldrig `login` direkt. _Node / TypeScript — getValidAccessToken_ ```typescript const PROACTIVE_REFRESH_MS = 36 * 60 * 60 * 1000; let inFlight: Promise | null = null; /** Single active session per account: one process, one token pair, one refresh at a time. */ export async function getValidAccessToken(): Promise { const current = await session.read(); if (current && Date.now() - current.rotatedAt < PROACTIVE_REFRESH_MS) { return current.accessToken; } // Single-flight guard: concurrent callers must not each burn a single-use refresh token. inFlight ??= (async () => { try { return current ? await rotate(current.refreshToken) : await login(); } catch (err) { if (err instanceof RefreshFailed) return await login(); // refresh token spent or expired throw err; } finally { inFlight = null; } })(); return inFlight; } /** Wrap every API call: one refresh, one replay, then surface the failure. */ export async function apiCall(path: string, init: RequestInit = {}) { let token = await getValidAccessToken(); let res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); if (res.status === 401) { await session.invalidateAccessToken(); token = await getValidAccessToken(); res = await fetch(`${BASE_URL}${path}`, withAuth(init, token)); } return res; // still 401 -> stop and alert an operator; do not loop } ``` _Python — get_valid_access_token_ ```python import threading, time _lock = threading.Lock() PROACTIVE_REFRESH_S = 36 * 3600 def get_valid_access_token() -> str: """One session per account: one lock, one refresh at a time.""" with _lock: current = session.read() if current and time.time() - current["rotated_at"] < PROACTIVE_REFRESH_S: return current["access_token"] try: return rotate(current["refresh_token"]) if current else login() except RefreshFailed: return login() # refresh token spent or expired -> full re-login ``` **Rotationen som sparar båda tokens** ```typescript async function rotate(currentRefreshToken: string) { const res = await fetch(`${BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: currentRefreshToken }), }); if (!res.ok) throw new RefreshFailed(res.status); const { AccessToken, RefreshToken } = await res.json(); // One write. Never persist the access token without the refresh token that came with it: // the token you sent is already dead, so a partial save loses the session for good. await session.replace({ accessToken: AccessToken, refreshToken: RefreshToken, rotatedAt: Date.now() }); return AccessToken; } ``` **Refresh-anropet i cURL** ```bash curl -X POST https://staging.zesec.com/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken": ""}' ``` ## 3. syncDevices Hämtar enhetslistan och skriver om mappningen. Körs på schema — inte i öppningsögonblicket. _cURL_ ```bash curl https://staging.zesec.com/api/user/device \ -H "Authorization: Bearer " ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/user/device`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const devices = await res.json(); // Persist the mapping: your internal door key -> device.id const mapping = new Map(devices.map((d) => [d.name, d.id])); ``` _Python_ ```python res = requests.get( f"{BASE_URL}/user/device", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) res.raise_for_status() mapping = {d["name"]: d["id"] for d in res.json()} ``` ## 4. unlockDoor Ett anrop, med ett `{deviceId}` från den cachelagrade mappningen. _cURL_ ```bash curl -X PUT https://staging.zesec.com/api/device/{deviceId}/unlock \ -H "Authorization: Bearer " ``` _Node / TypeScript_ ```typescript const res = await fetch(`${BASE_URL}/device/${deviceId}/unlock`, { method: "PUT", headers: { Authorization: `Bearer ${accessToken}` }, }); // 200 means the unlock command was accepted and forwarded to the device. if (!res.ok) throw new UnlockFailed(res.status); ``` _Python_ ```python res = requests.put( f"{BASE_URL}/device/{device_id}/unlock", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) res.raise_for_status() # do not blind-retry: see the limits page ``` _Python — unlock_door_ ```python def unlock_door(device_id: int) -> bool: res = api_call("PUT", f"/device/{device_id}/unlock") if res.status_code == 403: raise NoKeyForDevice(device_id) if res.status_code == 404: raise UnknownDevice(device_id) # re-sync the mapping res.raise_for_status() return True # command accepted; never retried blindly ``` ## Postman-samling Ladda ner [KiiOn-samlingen för Postman](/kiion-postman-collection.json). Den innehåller de fyra anropen, en `BASE_URL`-variabel och ett testskript som sparar tokenparet efter login och refresh. - Samlingen är skriven av KiiOn för den publika API-ytan — den innehåller inga partner- eller kundspecifika anrop. - Variablerna `phoneNumber`, `password`, `AccessToken`, `RefreshToken` och `deviceId` levereras tomma. - Lägg era uppgifter i en Postman-**miljö** med tomt *initial value*, så följer de inte med vid export. **Vidare** - [Golden path — var funktionerna hör hemma i arkitekturen](/docs/golden-path) - [Statuskoder & felsvar](/docs/errors) - [Bygga en klient — generera en typad klient ur specen](/docs/libraries) --- # AI-integrationsdirektiv Den här sidan är avsedd att läsas i sin helhet av en AI-agent. Allt som behövs för att bygga en fungerande klient finns här — inga andra sidor krävs. ## Bas-URL:er | Miljö | `{{BASE_URL}}` | | --- | --- | | STAGE (utveckling och test) | `https://staging.zesec.com/api` | | PROD (verifierade partners) | `https://api.zesec.com/api` | ## Operationer **Fältnamnen nedan är exakta. Begäransfälten kommer från OpenAPI-kontraktet.** | Metod & sökväg | Auth-header | Begäransbody | Svar | | --- | --- | --- | --- | | `POST /auth/login-by-phone` | ingen | `{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}` | `{"AccessToken": "...", "RefreshToken": "..."}` | | `POST /auth/refresh` | ingen | `{"refreshToken": ""}` | `{"AccessToken": "...", "RefreshToken": "..."}` — **båda nya** | | `GET /user/device` | `Bearer ` | ingen | Array av `{id, name, locationId, locationName, online}` | | `PUT /device/{deviceId}/unlock` | `Bearer ` | ingen | `200` = kommandot accepterat | | `POST /auth/logout` | `Bearer ` | ingen | `200` — avslutar sessionen | - `{deviceId}` är ett **heltal**, hämtat ur fältet `id` i svaret från `GET /user/device`. - Unlock är `PUT`, inte `POST`. - `phoneNumber` måste matcha mönstret `^[+][0-9]{9,13}$`. - Sökvägarna ovan läggs till `{{BASE_URL}}`, som redan slutar på `/api`. ## Tokenlivslängder - Access-token: ~48 timmar. Exakt utgång står i `exp` i JWT:n och är auktoritativ. - Refresh-token: upp till 30 dagar, men **engångs**. - Förnya proaktivt var 36:e timme. Vänta inte på ett fel. ## Felsignaler och åtgärd | Signal | Betydelse | Obligatorisk åtgärd | | --- | --- | --- | | `401` på ett vanligt anrop | Access-token utgången | Kör en refresh, gör om anropet **en** gång | | `400`/`401` på `/auth/refresh` | Refresh-token förbrukad eller utgången | Full ominloggning. Retry:a **aldrig** samma refresh-token | | `401` igen direkt efter refresh | Sessionen övertagen av annan inloggning | Stoppa. Larma. Loopa inte | | `403` på unlock | Kontot saknar nyckel till enheten | Stoppa. Detta löser sig inte med retry | | `404` på unlock | Okänt `{deviceId}` | Hämta enhetslistan på nytt och uppdatera mappningen | | `429` | För många anrop | Respektera `Retry-After`. Exponentiell backoff med jitter | | `5xx` | Tillfälligt fel | Backoff. Aldrig på unlock — se nedan | ## Hårda begränsningar 1. **En aktiv session per konto.** Implementera tokenlagret som en singleton med delad lagring. Två inloggningar med samma konto avlivar varandras sessioner. 2. **Refresh-token är engångs.** Ett samtidigt anropspar utan lås förbrukar två tokens och dödar sessionen. Single-flight-lås krävs. 3. **Skriv båda tokens atomiskt.** En halv skrivning förlorar sessionen permanent. 4. **Logga in server-side, aldrig per öppning.** Öppningsögonblicket ska innehålla exakt ett API-anrop. 5. **Retry:a aldrig unlock automatiskt.** Ett upprepat anrop kan öppna dörren igen. Visa felet för en människa. 6. **Hårdkoda aldrig `{deviceId}`.** Hämta och cachelagra mappningen. 7. **Tokens lämnar aldrig servern.** Inte till webbklient, inte till mobilapp, inte till loggar. ## Referensimplementation ```text STATE session = { accessToken, refreshToken, rotatedAt } # ONE per API account, server-side CONST PROACTIVE_REFRESH = 36 hours LOCK refreshLock # single-flight FUNCTION getValidAccessToken(): IF session EXISTS AND now() - session.rotatedAt < PROACTIVE_REFRESH: RETURN session.accessToken ACQUIRE refreshLock # concurrent callers wait, never race IF session EXISTS: TRY: RETURN rotate(session.refreshToken) CATCH RefreshFailed: # 400/401 => token spent or expired RETURN login() ELSE: RETURN login() RELEASE refreshLock FUNCTION rotate(refreshToken): res = POST {{BASE_URL}}/auth/refresh body {"refreshToken": refreshToken} IF res.status IN (400, 401): THROW RefreshFailed # do NOT retry this token, ever IF res.status >= 500: BACKOFF once, THEN THROW RefreshFailed session = { accessToken: res.AccessToken, refreshToken: res.RefreshToken, # BOTH, in ONE atomic write rotatedAt: now() } RETURN session.accessToken FUNCTION login(): res = POST {{BASE_URL}}/auth/login-by-phone body {"phoneNumber": ..., "password": ...} IF NOT res.ok: ALERT operator; STOP # never loop on a failed login session = { accessToken: res.AccessToken, refreshToken: res.RefreshToken, rotatedAt: now() } RETURN session.accessToken FUNCTION apiCall(method, path, body = null): res = REQUEST method, {{BASE_URL}} + path, Authorization: "Bearer " + getValidAccessToken() IF res.status == 401: invalidate session.accessToken res = REQUEST method, {{BASE_URL}} + path, Authorization: "Bearer " + getValidAccessToken() RETURN res # still 401 => STOP, alert; do not loop FUNCTION unlock(internalDoorId): ASSERT callerIsAuthorized(internalDoorId) # YOUR rules, not KiiOn's deviceId = cachedMapping[internalDoorId] # from syncDevices(), never hard-coded RETURN apiCall("PUT", "/device/" + deviceId + "/unlock") # exactly one call, no retry loop ``` ## Maskinläsbara källor - [/llms.txt — index över portalen](/llms.txt) - [/llms-full.txt — hela dokumentationen som en textfil](/llms-full.txt) - [/openapi.json — OpenAPI-dokumentet](/openapi.json) - [Färdiga prompter för att generera klienten](/docs/prompts) --- # AI-/Lovable-prompter Kör prompterna i ordning. Tillsammans producerar de mönstret på [Golden path](/docs/golden-path) — inte inloggning vid varje öppning. > **Byt bara ut platshållarna** — Prompterna innehåller inga riktiga uppgifter. Sätt bas-URL för rätt miljö och låt `{{phoneNumber}}`/`{{password}}` komma från miljövariabler. ## 1. KiiOn Auth Inloggning och säker server-side-lagring av tokenparet. ```text Build a KiiOn API client in . Base URL: https://staging.zesec.com/api Login: POST /auth/login-by-phone with body {"phoneNumber": "...", "password": "..."} Response: {"AccessToken": "...", "RefreshToken": "..."} Requirements: - Credentials come from environment variables, never from code or the repository. - The token pair is stored SERVER-SIDE in shared storage (database or cache), one record per API account. It must never reach a browser, a mobile app or a log line. - Expose exactly one entry point, getValidAccessToken(). No other code may call login(). - The account allows ONE active session, so the client must be a singleton per account. Do not implement refresh yet. Just login, storage and the singleton boundary. ``` ## 2. Token-förnyelse Proaktiv förnyelse, 401-fallback och roterande engångs-refresh-token. ```text Extend the client with token renewal. Refresh: POST /auth/refresh with body {"refreshToken": ""} Response: {"AccessToken": "...", "RefreshToken": "..."} — BOTH values are new. Requirements: - The refresh token is single-use. The one you sent is dead the instant the response arrives. Persist the new access token AND the new refresh token in ONE atomic write. - Refresh proactively every 36 hours, or earlier if the JWT "exp" claim says so. Do not wait for a 401. - Guard renewal with a single-flight lock: concurrent callers must await one refresh, never start two, or the second burns a token the first already consumed. - On 400/401 from /auth/refresh the token is spent — fall back to a full login. Never retry the same refresh token. - If the login fallback also fails, stop and surface the error. Do not loop. - Wrap API calls so a 401 triggers one refresh and one replay of the original request, then gives up. ``` ## 3. Enhetssynk + mappning Hämta enhetslistan och cachelagra mappningen mot era egna dörrar. ```text Add device synchronisation. Device list: GET /user/device with header "Authorization: Bearer " Response: array of {"id": , "name": "...", "locationId": , "locationName": "...", "online": } Requirements: - The "id" field is the {deviceId} used by the unlock endpoint. It is an integer. - Run syncDevices() on a schedule (roughly daily) and after any 404 from an unlock, never on the unlock path itself. - Persist a mapping from OUR internal door identifier to the KiiOn id, keeping name, location and a syncedAt timestamp for debugging. - Never hard-code a device id anywhere in the codebase. ``` ## 4. Unlock-funktion Ett anrop i öppningsögonblicket, med typade fel och audit. ```text Add the unlock function. Unlock: PUT /device/{deviceId}/unlock with header "Authorization: Bearer " A 200 means the command was accepted and forwarded to the device. Requirements: - The signature takes OUR internal door identifier, not a KiiOn id. - Check our own authorization rules first. KiiOn does not decide who may open. - Resolve {deviceId} from the cached mapping. Exactly one HTTP call happens here. - Never retry an unlock automatically — a repeated call can open the door a second time. On failure, return a typed error: 401 handled by the token layer, 403 = no key for this device, 404 = stale mapping (trigger a re-sync), 5xx = device or gateway unreachable. - Write every attempt to our own audit log with actor, door, deviceId and outcome. ``` ## 5. UI med öppna-knapp per dörr Ett gränssnitt som aldrig ser en token. ```text Build a UI that lists doors and gives each one an Open button. Requirements: - The UI talks ONLY to our own backend. It never sees a KiiOn token, base URL or device id. - List the doors the signed-in user is allowed to open, using our own authorization rules. - The Open button calls our backend endpoint, disables itself while the request is in flight and shows a clear result: opened, not permitted, door unreachable, or try again later. - No automatic retry on failure — the user decides whether to press again. - Show when a door is offline so the user is not left guessing. ``` **Vidare** - [AI-integrationsdirektiv — hela underlaget i ett svep](/docs/ai-instructions) - [/llms-full.txt — mata in hela dokumentationen i agenten](/llms-full.txt) --- # BLE SDK (steg 2) API-flödet är steg 1. BLE-SDK:t är steg 2 — mer arbete, och bara motiverat när uppkopplingen inte räcker. ## Så kopplar enheterna upp sig - **LTE** är den primära uppkopplingen. Enheten når KiiOn direkt över mobilnätet. - **WiFi** används som backup där sådan finns konfigurerad. - **BLE** finns som ytterligare fallback: telefonen i närheten av dörren låser upp lokalt, utan att enheten behöver vara online. För det stora flertalet integrationer räcker API-flödet. Enheten är uppkopplad, ert backend anropar unlock, dörren öppnas. ## När BLE är motiverat | Situation | Räcker API-flödet? | | --- | --- | | Dörren har stabil LTE eller WiFi | Ja — bygg inte BLE | | Källare, garage eller skärmade utrymmen utan täckning | Nej — BLE är fallbacken | | Krav på öppning även vid nätavbrott | Nej — BLE fungerar lokalt | | Ni vill minska latensen med några hundra millisekunder | Ja — inte värt komplexiteten | > **BLE är betydligt mer arbete** — BLE kräver en native mobilapp, hantering av Bluetooth-behörigheter, enhetsparning och plattformsspecifik felhantering på både iOS och Android. Bygg och verifiera API-flödet först — det är samma behörighetsmodell och samma tokenhantering i botten. ## Komma igång BLE-SDK:t distribueras separat och kräver onboarding. Hör av er via developers@kiion.io eller via [Support](/docs/support) och beskriv vilken plattform (iOS/Android), vilket användningsfall och vilken täckningssituation ni har. > **Ordningen spelar roll** — Kom till en fungerande upplåsning över API:t i STAGE innan ni ber om BLE-onboarding. Då blir samtalet om det som faktiskt är BLE-specifikt. **Vidare** - [Quickstart — API-flödet först](/docs/quickstart) - [Från sandbox till produktion](/docs/production) --- # Från sandbox till produktion Access till riktiga dörrar ges stegvis. Här är nivåerna, vad som krävs mellan dem och checklistan ni intygar innan ni får en PROD-nyckel. ## Tre accessnivåer | Nivå | Vad ni får | Vad som krävs | | --- | --- | --- | | **1. Publikt** | Hela den här dokumentationen och API-referensen | Ingenting. Inget avtal, ingen registrering. | | **2. Registrerad utvecklare** | STAGE-/sandbox-konto mot `https://staging.zesec.com/api` med testenheter | Ett lättviktigt click-through-[utvecklaravtal](/legal/terms). Ingen manuell NDA. | | **3. Verifierad partner** | PROD-access mot `https://api.zesec.com/api` med riktiga enheter | Ömsesidig NDA + [databehandlaravtal (DPA)](/legal/dpa), samt checklistan nedan. | > **Avtal gäller access, inte kunskap** — NDA och DPA hör hemma i steget till PROD, eftersom det är där riktiga enheter och persondata finns. Dokumentationen kräver inget avtal — se [Säkerhetsmodell](/docs/security-model#agreements). ## Självcertifiering före PROD Gå igenom listan och intyga varje punkt när ni ansöker om PROD-access. Punkterna är inte formaliteter — varje rad motsvarar ett fel vi sett i skarp drift. - [ ] Inloggning sker server-side - [ ] Proaktiv refresh implementerad - [ ] 401-fallback implementerad - [ ] Roterad refresh-token sparas korrekt - [ ] Enhetslistan cachelagras - [ ] Hanterar "en aktiv session per konto" - [ ] Felhantering/retry mot slutanvändare Varje punkt beskrivs i detalj på [Autentisering & tokens](/docs/authentication#checklist) och [Golden path](/docs/golden-path#checklist). ## Så ansöker ni Skicka en ansökan till developers@kiion.io, eller använd den kanal ni fått vid onboarding. Se [Få access](/docs/access) för formuläret och för hur ni får ert STAGE-konto. - Företag, organisationsnummer och teknisk kontaktperson. - Kort beskrivning av användningsfallet och ungefärlig volym upplåsningar per dygn. - Vilka platser/dörrar som ska omfattas. - Den ifyllda checklistan ovan. - Namngiven incidentkontakt som ska hållas aktuell efter driftsättning. ## Efter driftsättning - Håll incidentkontakten aktuell — se [Support](/docs/support) för eskaleringsvägar. - Använd separata API-konton för STAGE och PROD. Ett testskript mot produktionskontot slår ut produktionssessionen. - Följ [Changelog & status](/docs/changelog) för ändringar i den publika API-ytan. --- # Felsökning & FAQ Nästan alla supportärenden vi får är en av punkterna nedan. Leta upp symtomet, läs orsaken, gå till sidan som förhindrar det. ## Sessionen dör när flera klienter kör - **Symtom:** anrop börjar plötsligt ge `401` fastän token nyss fungerade, ofta i vågor. - **Orsak:** ett API-konto har en aktiv session. Varje ny inloggning avlivar den föregående — inklusive era egna testskript, en andra instans i klustret eller en kollegas lokala miljö. - **Åtgärd:** en singleton per konto, delad tokenlagring mellan instanser och separata konton per miljö. Se [Autentisering & tokens](/docs/authentication#one-session-per-account). ## Inloggning vid varje öppning - **Symtom:** långsamma öppningar, sporadiska `401` och dörrar som inte öppnas när två personer trycker samtidigt. - **Orsak:** varje öppning loggar in på nytt, vilket både lägger till latens och avlivar den föregående sessionen. - **Åtgärd:** logga in server-side en gång och återanvänd token. Öppningsögonblicket ska innehålla exakt ett anrop. Se [Golden path](/docs/golden-path#unlock-moment). ## Glömd roterad refresh-token - **Symtom:** förnyelsen fungerar en gång och misslyckas nästa gång, med `400` eller `401` från `/api/auth/refresh`. - **Orsak:** refresh-tokens är engångs. Svaret innehåller **både** en ny access-token och en ny refresh-token, och den gamla dör direkt. Sparas bara access-token är sessionen förlorad vid nästa förnyelse. - **Åtgärd:** skriv båda i samma atomiska operation. Se [Autentisering & tokens](/docs/authentication#token-refresh). ## Två samtidiga förnyelser - **Symtom:** sessionen dör under last, men fungerar i lugna perioder. - **Orsak:** två samtidiga `401` startar två parallella refresh-anrop. Det andra använder en token som det första just förbrukat. - **Åtgärd:** single-flight-lås runt förnyelsen. Se [Autentisering & tokens](/docs/authentication#failure-handling). ## Enhetslistan hämtas vid varje öppning - **Symtom:** onödig latens och fler anrop än nödvändigt, ibland `429`. - **Orsak:** enhetslistan hämtas i öppningsögonblicket i stället för på schema. - **Åtgärd:** cachelagra mappningen och uppdatera den på schema samt vid `404`. Se [Golden path](/docs/golden-path#device-mapping) och [Gränser & retry](/docs/limits). ## Fel eller föråldrat deviceId - **Symtom:** `404` på unlock för en dörr som brukade fungera. - **Orsak:** `{deviceId}` är hårdkodat, eller mappningen är gammal efter att en enhet bytts ut. - **Åtgärd:** kör om enhetssynken och uppdatera mappningen. Hårdkoda aldrig id:t. Se [Koncept & entitetsmodell](/docs/concepts#device-id). ## 403 på en dörr ni ser i listan - **Symtom:** enheten finns i `GET /api/user/device` men unlock ger `403`. - **Orsak:** nyckeln som ger öppningsrätt saknas eller har återkallats, även om enheten är synlig. - **Åtgärd:** kontakta KiiOn för att kontrollera nyckeldelningen. Retry hjälper inte. Se [Support](/docs/support). ## Rätt anrop, fel miljö - **Symtom:** `401` direkt efter en till synes lyckad inloggning, eller enhetslistan är tom. - **Orsak:** STAGE-uppgifter används mot PROD eller tvärtom. Kontona är separata. - **Åtgärd:** kontrollera `BASE_URL` mot kontot. Se [API-referens](/docs/api-reference#base-urls). ## Token gick ut mitt i drift - **Symtom:** allt fungerar i dagar och slutar sedan fungera. - **Orsak:** ingen proaktiv förnyelse — koden väntar på ett fel som kommer när access-token går ut. - **Åtgärd:** förnya var 36:e timme eller utifrån `exp`. Se [Autentisering & tokens](/docs/authentication#lifetimes). ## 200 på unlock men dörren öppnas inte - **Symtom:** API:t svarar `200`, dörren rör sig inte. - **Orsak:** `200` betyder att kommandot är accepterat och vidarebefordrat — inte att dörren fysiskt öppnats. Enheten kan vara offline eller ur räckvidd. - **Åtgärd:** läs enhetens tillstånd med `GET /api/device/{deviceId}` och kontrollera `online`. Se [Quickstart](/docs/quickstart#step-3-unlock). **Hittar du inte felet?** - [Statuskoder & felsvar — hela katalogen](/docs/errors) - [Support — kanaler och eskalering](/docs/support) --- # Changelog & status Här publiceras ändringar i den publika API-ytan och i den här portalen. Bryts något bakåtkompatibelt annonseras det här först. ## Driftstatus Aktuell driftstatus finns på [statussidan](https://developers.kiion.io/docs/changelog). Vid en pågående incident som påverkar upplåsningar i PROD, följ eskaleringsvägen på [Support](/docs/support). ## Så hanterar vi ändringar - **Additiva ändringar** (nya fält, nya endpoints) kan komma när som helst. Er klient ska tolerera okända fält. - **Deprecering** annonseras här innan den träder i kraft, med en angiven avvecklingsperiod. - **Brytande ändringar** annonseras här och kommuniceras dessutom direkt till verifierade partners. - Den maskinläsbara sanningen är alltid [`/openapi.json`](/openapi.json). Kör om er kodgenerering när något ändrats. ## Ändringar ### Portal 1.0 — Publik utvecklarportal (2026-08-21) - developers.kiion.io publicerad med de tretton guidesidorna enligt sitemap. - API-referensen genereras ur `/openapi.json` med både STAGE- och PROD-server. - Maskinläsbara källor tillagda: `/llms.txt`, `/llms-full.txt`, `/openapi.yaml` och `/openapi.pinned.json`. - Alla operationer dokumenterar nu sina 4xx-svar; se [Statuskoder & felsvar](/docs/errors). --- # Statuskoder & felsvar Skillnaden mellan "förnya token", "logga in på nytt" och "sluta försöka" är det som avgör om integrationen återhämtar sig eller låser ute kontot. ```json { "status": 401, "title": "Unauthorized", "detail": "The access token has expired." } ``` Behandla `status` som sanningen. Textfälten är avsedda för loggar och felsökning, inte för att matchas på i kod. ## Katalog | Status | Var | Vad som hänt | Klientens åtgärd | | --- | --- | --- | --- | | `400` | `/auth/refresh` | Refresh-token saknas, är förbrukad eller felformad | **Full ominloggning.** Retry:a aldrig samma token | | `400` | `/auth/login-by-phone` | `phoneNumber` matchar inte `^[+][0-9]{9,13}$`, eller fält saknas | Rätta begäran. Ingen retry | | `401` | Alla skyddade anrop | Access-token saknas, är utgången eller ogiltig | **Förnya en gång**, gör om anropet en gång | | `401` | `/auth/refresh` | Refresh-token förbrukad eller utgången | **Full ominloggning.** Aldrig retry på token | | `401` | Direkt efter lyckad förnyelse | Sessionen övertagen av en annan inloggning | **Sluta.** Larma. Loopa inte | | `403` | `/device/{deviceId}/*` | Kontot har ingen nyckel till enheten | **Sluta.** Kontakta KiiOn om nyckeldelning | | `404` | `/device/{deviceId}/*` | Okänt `{deviceId}`, eller enheten tillhör inte kontot | Hämta enhetslistan på nytt och uppdatera mappningen | | `409` | Auth | Sessionen ersattes av en samtidig inloggning | **Sluta.** Säkerställ en singleton per konto | | `429` | Alla | För många anrop | Respektera `Retry-After`. Exponentiell backoff med jitter | | `500` | Alla | Fel på serversidan | Backoff, försök igen — utom på unlock | | `502` / `503` / `504` | `/device/{deviceId}/unlock` | Enheten eller gatewayen svarar inte | **Retry:a inte automatiskt.** Visa felet för användaren | ## Tre regler som räcker 1. **`401` på ett vanligt anrop** = förnya och gör om en gång. Fortsätter det: sluta. 2. **`401`/`400` från `/auth/refresh`** = token är död. Logga in på nytt, aldrig retry. 3. **`403` och `404`** går inte att försöka sig ur. `403` kräver en nyckel, `404` kräver en ny mappning. > **Unlock är inte idempotent i praktiken** — Ett upprepat `PUT /device/{deviceId}/unlock` kan öppna dörren en gång till. Behandla den aldrig som säker att retry:a automatiskt — se [Gränser & retry](/docs/limits#unlock-retry). **Vidare** - [Beslutstabellen för tokenfel](/docs/authentication#failure-handling) - [Felsökning & FAQ — symtom och orsak](/docs/troubleshooting) - [/openapi.json — samma kontrakt, maskinläsbart](/openapi.json) --- # Gränser & retry API:t öppnar fysiska dörrar. Retry-policyn är därför inte en optimering utan en säkerhetsfråga. ## Är unlock säkert att försöka om? > **Nej. Retry:a aldrig unlock automatiskt.** — `PUT /device/{deviceId}/unlock` är inte idempotent i praktiken: varje accepterat anrop skickar ett nytt öppningskommando till enheten. Ett timeout betyder inte att kommandot uteblev — det kan ha nått fram. En automatisk retry kan därför öppna dörren en andra gång, vid en tidpunkt då ingen står där. - Vid fel: visa resultatet för en människa och låt personen trycka igen. Det är den enda säkra "retryn". - Logga varje försök i er egen audit-logg med tidsstämpel, aktör och `{deviceId}`. - Hur länge dörren står olåst efter en upplåsning är konfigurerat per enhet. Behöver ni veta det exakta värdet för era enheter, fråga KiiOn. ## Retry-policy per operation | Operation | Får försökas om? | Policy | | --- | --- | --- | | `GET /user/device` | Ja | Idempotent. Exponentiell backoff med jitter på `429`/`5xx` | | `GET /user/location` | Ja | Samma som ovan | | `GET /device/{deviceId}` | Ja | Samma som ovan | | `POST /auth/login-by-phone` | Begränsat | Högst ett omförsök vid `5xx`. Aldrig vid `400`/`401` — uppgifterna är fel | | `POST /auth/refresh` | Nej (samma token) | Vid `5xx`: ett omförsök. Vid `400`/`401`: ominloggning, aldrig samma token igen | | `PUT /device/{deviceId}/unlock` | **Nej** | Aldrig automatiskt. Visa felet | | `POST /auth/logout` | Ja | Ofarlig att upprepa | ```typescript const RETRYABLE = new Set([429, 500, 502, 503, 504]); async function withBackoff(fn: () => Promise, attempts = 4): Promise { for (let attempt = 0; ; attempt++) { const res = await fn(); if (!RETRYABLE.has(res.status) || attempt >= attempts - 1) return res; const retryAfter = Number(res.headers.get("Retry-After")) * 1000; const backoffMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : 2 ** attempt * 500 + Math.random() * 250; // jitter: never a synchronised thundering herd await sleep(backoffMs); } } // NOTE: never wrap PUT /device/{deviceId}/unlock in this. See "Is unlock safe to retry?". ``` ## Hur ofta ni bör anropa | Operation | Rekommenderad frekvens | Varför | | --- | --- | --- | | `POST /auth/login-by-phone` | En gång vid uppstart, samt som fallback | Varje inloggning avlivar den föregående sessionen | | `POST /auth/refresh` | Var 36:e timme | Proaktivt, inte som reaktion på ett fel | | `GET /user/device` | Var 24:e timme, samt vid `404` | Listan ändras sällan; cachelagra mappningen | | `PUT /device/{deviceId}/unlock` | En gång per faktisk öppning | Ingen polling, ingen spekulativ föröppning | > **Polla inte enhetslistan** — En hämtning per öppning ger onödig latens och riskerar throttling. Uppdatera mappningen på schema (24 h är en rimlig utgångspunkt) — se [Golden path](/docs/golden-path#device-mapping). ## Kvoter och throttling KiiOn publicerar i dagsläget inga numeriska anropstak per konto. Bygg därför klienten så att den hanterar throttling korrekt oavsett: respektera `429` och `Retry-After`, backa av exponentiellt och låt aldrig en retry-loop springa fritt. Behöver ni ett garanterat tak för ett högvolymsfall — hör av er till developers@kiion.io innan driftsättning. > **En retry-storm slår ut er egen session** — Upprepade inloggningar kolliderar med regeln om en aktiv session per konto. Ett skript som loopar på `login` kan låsa ute er produktionsintegration — se [Autentisering & tokens](/docs/authentication#one-session-per-account). **Vidare** - [Statuskoder & felsvar](/docs/errors) - [Samma regler som direktiv för AI-agenter](/docs/ai-instructions#failure-signals) --- # Bygga en klient KiiOn levererar inget eget SDK för API-flödet. Det behövs inte heller — specen är publik, så ni genererar en typad klient på en minut. ## Generera ur specen Portalen serverar OpenAPI-dokumentet på [`https://developers.kiion.io/openapi.json`](/openapi.json). Peka valfri generator på den. _TypeScript_ ```bash npx @hey-api/openapi-ts \ -i https://developers.kiion.io/openapi.json \ -o src/kiion \ -c @hey-api/client-fetch ``` _Swift_ ```bash npx @openapitools/openapi-generator-cli generate \ -i https://developers.kiion.io/openapi.json \ -g swift5 \ -o Sources/KiiOnAPI ``` _Rust_ ```bash npx @openapitools/openapi-generator-cli generate \ -i https://developers.kiion.io/openapi.json \ -g rust \ -o kiion-api ``` > **Pinna dokumentet i CI** — `/openapi.json` följer den levande källan. Vill ni att bygget ska vara reproducerbart, generera från `/openapi.pinned.json` i stället — den ändras bara när KiiOn medvetet uppdaterar den fastlåsta kopian. ```bash # The pinned document never changes under you, which is what a build wants. curl -fsSL https://developers.kiion.io/openapi.pinned.json -o openapi.pinned.json npx @hey-api/openapi-ts -i openapi.pinned.json -o src/kiion ``` ## Vad kodgenereringen ger — och inte ger | Ingår i den genererade klienten | Måste ni lägga till själva | | --- | --- | | Typade request- och responsmodeller | Server-side singleton per API-konto | | Rätt HTTP-metod och sökväg per operation | Proaktiv token-förnyelse med single-flight-lås | | Bas-URL:er för STAGE och PROD | Atomisk lagring av det roterade tokenparet | | Bearer-headern som parameter | 401-fallback som gör om anropet en gång | | | Cachelagrad mappning från era dörrar till `{deviceId}` | | | Regeln att unlock aldrig retry:as automatiskt | > **En genererad klient är transport, inte sessionshantering** — Alla fallgropar i den här integrationen ligger i lagret ovanför. Wrappa den genererade klienten med tokenhanteraren från [Kodexempel](/docs/code-examples#get-valid-access-token) — annars loggar den in per anrop. ## Om ni hellre skriver klienten för hand API:t är vanlig JSON över HTTPS — vilket HTTP-bibliotek som helst duger. Beprövade val: | Språk | Bibliotek | Installation | | --- | --- | --- | | Node.js | [undici](https://undici.nodejs.org/) | `npm i undici` (eller inbyggda `fetch`) | | Python | [requests](https://requests.readthedocs.io/) | `pip install requests` | | Rust | [reqwest](https://docs.rs/reqwest/) + [serde_json](https://docs.rs/serde_json/) | `cargo add reqwest serde_json tokio` | | Swift | URLSession + `Codable` | Ingår i Foundation | | PHP | [Guzzle](https://docs.guzzlephp.org/) | `composer require guzzlehttp/guzzle` | | Ruby | [Faraday](https://lostisland.github.io/faraday/) | `gem install faraday` | **Vidare** - [Kodexempel — de fyra funktionerna, färdiga att kopiera](/docs/code-examples) - [API-referens — alla operationer](/docs/api-reference) --- # Få access Dokumentationen är öppen, men API-konton tilldelas av KiiOn. Här är exakt hur ni begär ett. > **Registrera er själva** — Sandbox-registreringen är självbetjäning och ligger på [integrationssidan på kiion.io](https://www.kiion.io/integration). Behöver ni något som formuläret inte täcker — ett särskilt upplägg, fler testenheter — mejla developers@kiion.io. Resten av den här sidan beskriver vad ni behöver ha redo, oavsett väg. ## Vad ni skickar in Mejla developers@kiion.io med rubriken "STAGE-access" och följande uppgifter: - **Företag** och organisationsnummer. - **Teknisk kontaktperson** — namn, e-post och telefon. - **Användningsfall** — vad ni bygger och hur upplåsningen ska triggas. - **Vilka platser/dörrar** integrationen ska omfatta, eller att det är rent testbruk tills vidare. - **Ungefärlig volym** upplåsningar per dygn när ni är i drift. - **Upplägg** — ska era kunders dörrar ligga under ert företag, eller ska nycklar delas till ert API-konto? Se [Koncept & entitetsmodell](/docs/concepts#key-sharing) om ni är osäkra; vi hjälper er välja. ## Vad ni får tillbaka - Ett STAGE-API-konto: `{{phoneNumber}}` och `{{password}}` mot `https://staging.zesec.com/api`. - En eller flera testenheter som kontot har nyckel till. - Ett click-through-[utvecklaravtal](/legal/terms) att godkänna. Ingen manuell NDA för sandbox. Uppgifterna skickas i en separat kanal från ansökan. Lägg dem direkt i er hemlighetshantering — aldrig i kod, aldrig i ett ärendesystem. ## Handläggningstid Räkna med svar inom ett par arbetsdagar. Kompletta ansökningar går snabbare — de vanligaste kompletteringsfrågorna gäller upplägget och vilka dörrar som ska omfattas. ## Sedan då? 1. Kör [Quickstart](/docs/quickstart) — tre anrop till en lyckad upplåsning i STAGE. 2. Bygg enligt [Golden path](/docs/golden-path). 3. Gå igenom checklistan på [Från sandbox till produktion](/docs/production#checklist) och ansök om PROD-access. - [Support — frågor under utvecklingen](/docs/support) - [Säkerhetsmodell — varför access tilldelas av KiiOn](/docs/security-model) --- # Support Två olika kanaler för två olika situationer: frågor under utvecklingen, och dörrar som inte öppnas i skarp drift. > **Börja här** — Ungefär nio av tio ärenden vi får står redan beskrivna på [Felsökning & FAQ](/docs/troubleshooting). Titta där först — det går fortare än att vänta på svar. ## Kanaler | Situation | Kanal | Förväntad första respons | | --- | --- | --- | | Frågor om dokumentation, STAGE eller integrationsdesign | developers@kiion.io | Inom en arbetsdag | | Ansökan om STAGE- eller PROD-access | developers@kiion.io, se [Få access](/docs/access) | Ett par arbetsdagar | | Misstänkt säkerhetsproblem | Se [Ansvarsfull rapportering](/docs/disclosure) | Inom tre arbetsdagar | | **PROD-incident: dörrar öppnas inte** | Den incidentkanal ni fått vid go-live | Enligt ert avtal | ## Allvarlighetsgrader i PROD | Grad | Innebär | Exempel | | --- | --- | --- | | **S1** | Upplåsning fungerar inte alls för en eller flera platser | Alla dörrar på en plats svarar `5xx` | | **S2** | Delvis avbrott eller kraftigt försämrad funktion | Enstaka enheter offline, kraftigt ökad latens | | **S3** | Begränsad påverkan, går att arbeta runt | En enskild enhet svarar inte | | **S4** | Ingen driftpåverkan | Fråga om dokumentation eller framtida ändring | Ta med följande vid en incident: tidpunkt (med tidszon), berörda `{deviceId}`, den statuskod och det felmeddelande ni får, samt om felet gäller alla dörrar eller enstaka. > **Skicka aldrig med tokens eller lösenord** — Vi behöver aldrig era uppgifter för att felsöka. Skicka statuskoder, tidsstämplar och `{deviceId}` — aldrig ``, `` eller `{{password}}`. ## Efter driftsättning - Håll en namngiven integrations- och incidentkontakt aktuell hos KiiOn. Byter någon roll, meddela oss. - Följ [Changelog & status](/docs/changelog) och [statussidan](https://developers.kiion.io/docs/changelog). - Använd separata API-konton för STAGE och PROD, annars slår er testning ut produktionssessionen. --- # Ansvarsfull rapportering Vi publicerar hela den publika API-ytan öppet. Motsvarigheten till det är en tydlig kanal för den som hittar ett problem. ## Så rapporterar ni - **Kanal:** security@kiion.io - **Maskinläsbart:** [`https://developers.kiion.io/.well-known/security.txt`](/.well-known/security.txt) enligt RFC 9116. - **Språk:** svenska eller engelska. - **Ta med:** vad ni hittade, hur det reproduceras, vilken miljö och vilken påverkan ni bedömer att det har. > **Skicka aldrig riktiga uppgifter i rapporten** — Beskriv sårbarheten, inte era eller någon annans inloggningsuppgifter. Behöver vi en token för att reproducera hör vi av oss i en separat kanal. ## Vad ni kan förvänta er | Steg | Tid | | --- | --- | | Bekräftelse på att rapporten mottagits | Inom tre arbetsdagar | | Första bedömning av allvarlighetsgrad | Inom tio arbetsdagar | | Löpande statusuppdatering | Tills ärendet är stängt | ## Scope **Ingår:** `developers.kiion.io`, den publika API-ytan i STAGE, och den publicerade OpenAPI-specen. > **Testning sker mot STAGE — aldrig mot riktiga dörrar** — PROD-miljön och installerade enheter är utanför scope. Sandbox-uppgifter får aldrig användas för fysiskt intrångstest, och ni får aldrig försöka låsa upp en dörr ni inte äger eller har uttryckligt tillstånd att testa mot. ## Utanför scope - Överbelastningsattacker (DoS/DDoS) och lasttestning. - Social engineering mot KiiOns personal, partners eller kunder. - Fysisk manipulation av installerad hårdvara. - Automatiserade skannerrapporter utan verifierad påverkan. - Avsaknad av härdningsheaders utan demonstrerad utnyttjbarhet. ## Safe harbour Håller ni er inom scope, undviker att komma åt eller ändra andras data, och ger oss rimlig tid att åtgärda innan ni publicerar, betraktar vi rapporteringen som ett bidrag och vidtar inga rättsliga åtgärder. Vi driver inget bug bounty-program med penningbelöning. Rapportörer som vill får gärna omnämnas när fixen är ute. --- # Utvecklaravtal Sandbox-access omfattas av ett lättviktigt utvecklaravtal som godkänns när STAGE-kontot utfärdas. > **Status** — Det bindande avtalstextsdokumentet levereras tillsammans med STAGE-kontot och godkänns då. Vill ni granska det i förväg — inför en juridisk genomgång eller en upphandling — begär det via developers@kiion.io. Sammanfattningen nedan beskriver innebörden men ersätter inte avtalstexten. ## Vad avtalet omfattar - Rätt att använda STAGE-miljön och tilldelade testenheter för utveckling och test av en integration. - Skyldighet att hantera tilldelade uppgifter (`{{phoneNumber}}`, `{{password}}`) som hemligheter och lagra dem server-side. - Förbud mot att använda sandbox-access mot riktiga installerade enheter eller för intrångstest utanför scope — se [Ansvarsfull rapportering](/docs/disclosure#scope). - Ingen utfästelse om tillgänglighet i STAGE. Miljön är avsedd för test. - Avtalet gäller access, inte dokumentationen. Dokumentationen är öppen för alla, utan avtal. ## Vad det inte omfattar PROD-access mot riktiga enheter. Det kräver ömsesidig NDA och ett [databehandlaravtal](/legal/dpa) — se [Från sandbox till produktion](/docs/production#tiers). --- # Databehandlaravtal (DPA) I produktion hanteras persondata. Därför tecknas ett databehandlaravtal innan PROD-access ges. > **Så begär ni avtalet** — Mejla developers@kiion.io med rubriken "DPA" och uppgifter om företag samt kontaktperson för dataskyddsfrågor. Er compliance-funktion får dokumentet inför granskning, före PROD-driftsättning. ## Varför ett DPA behövs En upplåsning knyter en identitet till en dörr vid en tidpunkt. Det är persondata enligt GDPR. När ni driftsätter mot riktiga enheter behöver rollerna — vem som är personuppgiftsansvarig och vem som är personuppgiftsbiträde — och instruktionerna för behandlingen vara dokumenterade. ## Vad avtalet typiskt reglerar - Föremålet för behandlingen, varaktighet, art och ändamål. - Kategorier av registrerade och typer av persondata. - Instruktioner för behandlingen och begränsningar för underbiträden. - Tekniska och organisatoriska säkerhetsåtgärder. - Rutin vid personuppgiftsincident och biträdets biståndsskyldighet. - Radering eller återlämnande av data när avtalet upphör. - Överföringar till tredjeland, om tillämpligt. > **Det här är en sammanfattning** — Innehållet ovan beskriver vad ett DPA av den här typen normalt reglerar. Det bindande innehållet är det undertecknade dokumentet, inte den här sidan. ## Er egen behandling Ni avgör vem som får öppna vilken dörr och när, och det är er behörighetslogik och er audit-logg som avgör vilka persondata ni själva behandlar. Se [Golden path](/docs/golden-path#layers) för ansvarsfördelningen mellan lagren. --- # Integritetspolicy Den här portalen är publik och kräver ingen inloggning. Nedan står exakt vad som lagras. ## Personuppgiftsansvarig KiiOn (Zesec) är personuppgiftsansvarig för behandlingen på den här webbplatsen. Dataskyddsfrågor: developers@kiion.io. Säkerhetsärenden: security@kiion.io. ## Vad portalen lagrar | Vad | Var | Varför | Livslängd | | --- | --- | --- | --- | | Valt språk (`sv`/`en`) | Cookie i er webbläsare | Så att sidan visas på rätt språk vid nästa besök | Ett år | | Valt tema (ljust/mörkt/system) | `localStorage` i er webbläsare | Så att utseendet består mellan besök | Tills ni rensar webbläsardata | | Åtkomstloggar (IP, tidpunkt, sökväg, user agent) | Serversidan | Drift, felsökning och att upptäcka missbruk | Kort period för driftändamål | > **Ingen spårning** — Portalen använder inga analysverktyg, inga annonsnätverk, inga tredjeparts-cookies och ingen inbäddning från externa domäner. Språkcookien och temanyckeln är rent funktionella — de gör inget annat än att komma ihåg era val. ## Vad API:t behandlar Det här avsnittet gäller KiiOn-API:t, inte den här webbplatsen. En upplåsning innebär att en identitet knyts till en dörr vid en tidpunkt. Vid produktionsdrift regleras behandlingen av ett [databehandlaravtal](/legal/dpa), där rollfördelning, kategorier av registrerade och säkerhetsåtgärder anges. ## Era rättigheter - Rätt till tillgång, rättelse och radering. - Rätt till begränsning av och invändning mot behandling. - Rätt till dataportabilitet där behandlingen grundas på avtal eller samtycke. - Rätt att lämna klagomål till Integritetsskyddsmyndigheten (IMY). Utöva rättigheterna genom att kontakta developers@kiion.io. Gäller det data som behandlas i en integratörs egen tjänst är det den integratören som är rätt mottagare. ## Rensa era val Rensa webbplatsdata för `developers.kiion.io` i webbläsaren så tas både språkcookien och temanyckeln bort. Inget annat påverkas — portalen har inga konton. ---