{"openapi":"3.0.1","info":{"title":"KiiOn API","version":"v1","description":"Pinned fallback OpenAPI document for developers.kiion.io, generated by `npm run openapi:sync` from the live zesec.api swagger through the same filter/overlay pipeline the runtime uses. Served only when the live source is unreachable. Do not hand-edit — re-run the sync instead."},"paths":{"/api/auth/login":{"post":{"tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestDTO"}}},"required":true},"responses":{"200":{"description":"A new token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenPair"}}}},"400":{"description":"Malformed request body, or phoneNumber does not match ^[+][0-9]{9,13}$.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Wrong credentials. Do not retry with the same values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"login","summary":"Log in (phone or email)","description":"Generic login. Most integrations should use `POST /api/auth/login-by-phone`. One active session per account: a new login invalidates the previous session."}},"/api/auth/login-by-phone":{"post":{"tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestDTO"}}},"required":true},"responses":{"200":{"description":"A new token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenPair"}}}},"400":{"description":"Malformed request body, or phoneNumber does not match ^[+][0-9]{9,13}$.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Wrong credentials. Do not retry with the same values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"loginByPhone","summary":"Log in with phone number and password","description":"Returns an access token (JWT) and a rotating single-use refresh token. Log in server-side once and reuse the token — never per unlock. One active session per account."}},"/api/auth/login-by-email":{"post":{"tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginByEmailRequestDTO"}}},"required":true},"responses":{"200":{"description":"A new token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenPair"}}}},"400":{"description":"Malformed request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Wrong credentials. Do not retry with the same values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"loginByEmail","summary":"Log in with email and password"}},"/api/auth/refresh":{"post":{"tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshSessionRequestDTO"}}},"required":true},"responses":{"200":{"description":"A new token pair. Both values are new; the old refresh token is already dead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenPair"}}}},"400":{"description":"The refresh token is missing, malformed or already consumed. Do NOT retry it — run a full login instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The refresh token is expired or revoked. Run a full login (POST /api/auth/login-by-phone). Retrying this token will never succeed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"refreshToken","summary":"Refresh the session (rotating, single-use)","description":"Exchanges the current refresh token for a NEW access token and a NEW refresh token. The token you send is invalid the instant the response is issued — persist both returned values in one atomic write. Never retry a refresh token that has already been sent."}},"/api/auth/logout":{"post":{"tags":["Auth"],"responses":{"200":{"description":"The session was ended."},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"logout","summary":"End the current session","description":"Releases the single active session held by the account. Requires `Authorization: Bearer <AccessToken>`."}},"/api/device/{deviceId}":{"get":{"tags":["Device"],"parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"The device's current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Device"}}}},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The account holds no key to this device. Retrying will not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown deviceId, or it does not belong to this account. Re-fetch GET /api/user/device.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getDevice","summary":"Read a device's current state","description":"Use this to confirm what happened after an unlock — a 200 from the unlock operation means the command was accepted, not that the door physically opened."}},"/api/device/{deviceId}/unlock":{"put":{"tags":["Device"],"parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"The unlock command was accepted and forwarded to the device."},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The account holds no key to this device. Retrying will not help; contact KiiOn about key sharing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown deviceId. Re-fetch GET /api/user/device and update your mapping.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"The device or its gateway is unreachable. Do not auto-retry — report the failure to the user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The device is offline or out of range. Do not auto-retry — report the failure to the user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"504":{"description":"The device did not answer in time. The command may still have arrived; do not auto-retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"unlockDevice","summary":"Unlock a device","description":"Sends an unlock command to the device. A 200 means the command was ACCEPTED and forwarded — not that the door physically opened; read GET /api/device/{deviceId} to confirm state. NOT safe to retry automatically: a repeated call can open the door a second time. On failure, surface it to a human."}},"/api/device/{deviceId}/unlock/{lockId}":{"put":{"tags":["Device"],"parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"lockId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"The unlock command was accepted and forwarded to the device."},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The account holds no key to this device or lock.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown deviceId or lockId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"The device or its gateway is unreachable. Do not auto-retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The device is offline or out of range. Do not auto-retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"504":{"description":"The device did not answer in time. Do not auto-retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"unlockDeviceLock","summary":"Unlock one specific lock on a multi-lock device","description":"Only for devices carrying more than one lock. Single-lock devices use PUT /api/device/{deviceId}/unlock. The same no-auto-retry rule applies."}},"/api/user/location":{"get":{"tags":["User"],"responses":{"200":{"description":"The locations visible to this account."},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"listUserLocations","summary":"List the locations this account can reach"}},"/api/user/device":{"get":{"tags":["User"],"responses":{"200":{"description":"The devices this account owns or holds a shared key to.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Device"}},"example":[{"id":1001,"name":"Door A","locationId":42,"locationName":"Location 1","companyName":"Example Inc.","doorType":"Entrance","online":true}]}}},"401":{"description":"Access token missing, expired or invalid. Call POST /api/auth/refresh once, then replay this request. If it returns 401 again, the session was superseded — stop and alert an operator; do not loop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests. Honour Retry-After, then back off exponentially with jitter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"listUserDevices","summary":"List the devices this account can reach","description":"The `id` of each entry is the `{deviceId}` used by the unlock operations. Cache this mapping — do not call it on the unlock path, and never hard-code an id."}}},"components":{"schemas":{"Device":{"type":"object","description":"A door/device this account can reach. `id` fills `{deviceId}` in the unlock operations.","properties":{"id":{"type":"integer","format":"int32","description":"The deviceId used by the unlock operations."},"name":{"type":"string","nullable":true,"description":"The door's name. Map this to your own door identity."},"locationId":{"type":"integer","format":"int32","nullable":true},"locationName":{"type":"string","nullable":true},"companyName":{"type":"string","nullable":true},"doorType":{"type":"string","nullable":true},"online":{"type":"boolean","nullable":true,"description":"Whether the device is currently reachable."}},"required":["id"],"example":{"id":1001,"name":"Door A","locationId":42,"locationName":"Location 1","companyName":"Example Inc.","doorType":"Entrance","online":true}},"ErrorResponse":{"type":"object","description":"Error payload. Branch on `status`; the text fields are for logs, not for matching.","properties":{"status":{"type":"integer","format":"int32"},"title":{"type":"string","nullable":true},"detail":{"type":"string","nullable":true}},"required":["status"],"example":{"status":401,"title":"Unauthorized","detail":"The access token has expired."}},"LoginByEmailRequestDTO":{"required":["email","password"],"type":"object","properties":{"email":{"minLength":1,"type":"string","format":"email"},"password":{"minLength":1,"type":"string"}},"additionalProperties":false},"LoginRequestDTO":{"required":["password","phoneNumber"],"type":"object","properties":{"phoneNumber":{"minLength":1,"pattern":"^[+][0-9]{9,13}$","type":"string","format":"tel"},"password":{"minLength":1,"type":"string"}},"additionalProperties":false},"RefreshSessionRequestDTO":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"minLength":1,"type":"string"}},"additionalProperties":false},"TokenPair":{"type":"object","description":"A session's token pair. Both values change on every refresh; persist them together in one atomic write.","properties":{"AccessToken":{"type":"string","description":"JWT sent as `Authorization: Bearer <AccessToken>`. Its exact expiry is the `exp` claim."},"RefreshToken":{"type":"string","description":"Single-use. Consumed by the next POST /api/auth/refresh."}},"required":["AccessToken","RefreshToken"],"example":{"AccessToken":"<AccessToken>","RefreshToken":"<RefreshToken>"}}},"securitySchemes":{"Bearer":{"type":"apiKey","description":"JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"","name":"Authorization","in":"header"}}},"security":[{"Bearer":[]}],"servers":[{"url":"https://staging.zesec.com","description":"STAGE (test) — integrators develop and test here"},{"url":"https://api.zesec.com","description":"PROD — verified partners only"}]}