# igloo Platform Monolithic Context Corpus > This file contains the entire documentation suite inlined for AI agents and LLMs. ## API Reference ### FILE: home/api_access Title: Access Category: API Reference ---------------------------------------- # Access API Reference API endpoints for managing access. ## AlgoPIN ### Create a Daily AlgoPIN `POST` `https://api.igloohome.co/home/devices/{deviceId}/algopin/daily` Returns a duration (Daily) AlgoPIN code. Allowable Range: 29 to 367 days (in days only, 29.5 days not allowed) #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required* - Allowed values: `1`, `2`, `3` - **`startDate`** `string` *required*\ Date and time when the permission starts. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). **Note: The hour ('hh') on startDate and the hour ('hh') on endDate must match** - Example: `2022-01-01T12:00:00+08:00` - **`endDate`** `string` *required*\ Date and time when the permission ends. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). **Note: The hour ('hh') on startDate and the hour ('hh') on endDate must match** - Example: `2022-05-01T12:00:00+08:00` - **`accessName`** `string` *required*\ Name or short description of the access - Example: `Maintenance guy` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily" payload := []byte(`{ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/algopin/daily", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/algopin/daily") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/algopin-daily'` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard).` `415 Unsupported Media Type` `418 '{ error: 'this device type does not support the feature.' }'. The device SKU does not support AlgoPIN codes.` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string` - Example: `4642226` - **`pinId`** `string` - Example: `CC0035C33E9B103A` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Response body — `418`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "7541869", "pinId": "EE4421C77G9F512D" } ``` **Example response — `400`:** ```json { "error": "'accessName' is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` **Example response — `418`:** ```json { "error": "this device type does not support the feature." } ``` ### Create an Hourly AlgoPIN `POST` `https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly` Returns a duration (Hourly) AlgoPIN code. Allowable Range: 1 to 672 hours #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required* - Allowed values: `1`, `2`, `3` - **`startDate`** `string` *required*\ Date and time when the permission starts. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). May be up to 1 hour in the past. - Example: `2022-01-01T14:00:00+08:00` - **`endDate`** `string` *required*\ Date and time when the permission ends. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). **Maximum Value: startDate + 672 hours** - Example: `2022-01-02T12:00:00+08:00` - **`accessName`** `string` *required*\ Name or short description of the access - Example: `Maintenance guy` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly" payload := []byte(`{ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/algopin-hourly'` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard).` `415 Unsupported Media Type` `418 '{ error: 'this device type does not support the feature.' }'. The device SKU does not support AlgoPIN codes.` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string` - Example: `4642226` - **`pinId`** `string` - Example: `CC0035C33E9B103A` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Response body — `418`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "5729314", "pinId": "AA9876B22D5E405C" } ``` **Example response — `400`:** ```json { "error": "'accessName' is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` **Example response — `418`:** ```json { "error": "this device type does not support the feature." } ``` ### Create a One-Time AlgoPIN `POST` `https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime` Returns a one-time AlgoPIN code (OTP). #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required*\ 1–10. Values 6–10 require newer lock firmware; some older locks support them after a firmware update, otherwise they accept 1–5. - **`startDate`** `string` *required*\ Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only) and now or later. - Example: `2022-01-01T00:00:00+08:00` - **`accessName`** `string` *required* - Example: `Maintenance guy` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime" payload := []byte(`{ "accessName": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/algopin-onetime'` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard).` `415 Unsupported Media Type` `418 '{ error: 'this device type does not support the feature.' }'. The device SKU does not support AlgoPIN codes.` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string` - Example: `4642226` - **`pinId`** `string` - Example: `CC0035C33E9B103A` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Response body — `418`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "4642226", "pinId": "CC0035C33E9B103A" } ``` **Example response — `400`:** ```json { "error": "'accessName' is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` **Example response — `418`:** ```json { "error": "this device type does not support the feature." } ``` ### Create a Passage AlgoPIN `POST` `https://api.igloohome.co/home/devices/{deviceId}/algopin/passage` Allows unlock with AlgoPIN and keeps the lock unlocked for the duration given in the request body. Supports newer devices with the latest firmware only. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required* - Allowed values: `1` - **`startDate`** `string` *required*\ Date and time when the permission starts. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). The PIN is valid from `startDate` for `durationHours` hours. - Example: `2022-01-01T12:00:00+08:00` - **`durationHours`** `integer` *required*\ Must be between 1 and 12 hours - **`accessName`** `string` *required*\ Name or short description of the access - Example: `Maintenance guy` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage" payload := []byte(`{ "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/algopin/passage", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "durationHours" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/algopin/passage") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "durationHours": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard).` `415 Unsupported Media Type` `422 '{ error: 'this device does not support passage PIN' }'. The device does not support passage AlgoPIN codes.` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string` - Example: `4642226` - **`pinId`** `string` - Example: `CC0035C33E9B103A` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Response body — `422`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "7541869", "pinId": "EE4421C77G9F512D" } ``` **Example response — `400`:** ```json { "error": "'accessName' is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` **Example response — `422`:** ```json { "error": "this device does not support passage PIN" } ``` ### Create a Permanent AlgoPIN `POST` `https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent` Returns a permanent AlgoPIN code. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required*\ 1–10. Values 6–10 require newer lock firmware; some older locks support them after a firmware update, otherwise they accept 1–5. - **`startDate`** `string` *required*\ Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only) and now or later. - Example: `2022-01-01T00:00:00+08:00` - **`accessName`** `string` *required* - Example: `Maintenance guy` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent" payload := []byte(`{ "accessName": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/algopin-permanent'` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard).` `415 Unsupported Media Type` `418 '{ error: 'this device type does not support the feature.' }'. The device SKU does not support AlgoPIN codes.` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string` - Example: `4642226` - **`pinId`** `string` - Example: `CC0035C33E9B103A` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Response body — `418`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "8137492", "pinId": "DD1246E44F0C214B" } ``` **Example response — `400`:** ```json { "error": "'accessName' is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` **Example response — `418`:** ```json { "error": "this device type does not support the feature." } ``` ## eKey ### Generate eKey `POST` `https://api.igloohome.co/home/devices/{deviceId}/ekey` Create an eKey. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`startDate`** `string` *required*\ User is able to control the lock starting this date. Seconds must be `:00` (minute resolution). - Example: `2021-01-03T12:00:00+08:00` - **`endDate`** `string` *required*\ User is unable to control the lock starting this date. Seconds must be `:00` (minute resolution), and must be after `startDate`. - Example: `2021-11-03T12:00:00+08:00` - **`permissions`** `array of string` *required*\ List of permissions to grant on this eKey. Required. There is no default, although the field can look optional. - Example: `["UNLOCK"]` #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/ekey" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "endDate": "value", "permissions": "value", "startDate": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/ekey" payload := []byte(`{ "endDate": "value", "permissions": "value", "startDate": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/ekey", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "endDate": "value", "permissions": "value", "startDate": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/ekey" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "endDate": "value", "permissions": "value", "startDate": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/ekey", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "endDate" => "value", "permissions" => "value", "startDate" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "endDate": "value", "permissions": "value", "startDate": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/ekey") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/ekey")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "endDate": "value", "permissions": "value", "startDate": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/create-ekey-access'` `404 '{ message: 'not found device' }' or '{ message: 'not found property' }'` `409 Response` `415 Unsupported Media Type` `500 Internal Server Error` **Response body — `200`:** - **`keyId`** `integer` - **`bluetoothGuestKey`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Example response — `200`:** ```json { "bluetoothGuestKey": "xNXdtaE5DcjBzbktXZWc1Q3d2SFd0RVU5T2swWjRmQjRkRjdhczlONEE9PQ==", "keyId": 12847 } ``` **Example response — `400`:** ```json "configuration lock issue" ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json "invalid.generate.ekey" ``` ## iglooKey ### Create iglooKey Token `POST` `https://api.igloohome.co/home/pass` Creates a shareable iglooKey access token that supports both online and offline unlock for one or more devices. #### Body Parameters Content-Type: `application/json` - **`deviceIds`** `array of string` *required* - **`label`** `string` *required* - **`endsAt`** `string` *required*\ Must be in the future - **`startsAt`** `string`\ Must be on or before `endsAt` - **`activationWindowSeconds`** `integer`\ If set, the token must be activated within this many seconds of `startsAt` (or now); the resulting deadline must not exceed `endsAt`. - **`offlineAccessSeconds`** `integer`\ Grants offline access validity, in seconds (max 24 hours) #### Request Example ```bash curl -X POST "https://api.igloohome.co/home/pass" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/pass" payload := []byte(`{ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/pass", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/pass" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/pass", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "activationWindowSeconds" => "value", "deviceIds" => "value", "endsAt" => "value", "label" => "value", "offlineAccessSeconds" => "value", "startsAt" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/pass") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/pass")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "offlineAccessSeconds": "value", "startsAt": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `201 Created` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden` `404 Not Found` `415 Unsupported Media Type` `422 '{ message: 'device type not supported for iglooKey', excludedIds: string[] }'` `500 Internal Server Error` `502 Response` `503 Response` **Response body — `201`:** - **`shareableUrl`** `string` - **`id`** `string` - **`type`** `string`\ Token type, `TOKEN_TYPE_HYBRID`. - **`endsAt`** `string` - **`createdAt`** `string` - **`startsAt`** `string` - **`activationDeadline`** `string` **Response body — `400`:** - **`error`** `string` - **`message`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`missingIds`** `array of string` **Example response — `201`:** ```json { "activationDeadline": "2025-09-15T11:00:00Z", "createdAt": "2025-09-08T14:30:45Z", "endsAt": "2025-12-31T23:59:59Z", "id": "lnk_abc123def456ghi789jkl012mno345", "shareableUrl": "https://igloodev.app.link/pass?token=lnk_abc123def456ghi789jkl012mno345", "startsAt": "2025-09-15T10:00:00Z", "type": "TOKEN_TYPE_HYBRID" } ``` **Example response — `400`:** ```json { "error": "\"endsAt\" must be in the future" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "device not found" } ``` ### Update iglooKey Token `PATCH` `https://api.igloohome.co/home/pass/{id}` Updates the validity window (`startsAt`/`endsAt`) of an existing iglooKey token. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`startsAt`** `string`\ Must be in the future - **`endsAt`** `string`\ Must be in the future and after `startsAt` #### Request Example ```bash curl -X PATCH "https://api.igloohome.co/home/pass/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "endsAt": "value", "startsAt": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/pass/{id}" payload := []byte(`{ "endsAt": "value", "startsAt": "value" }`) req, _ := http.NewRequest("PATCH", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/pass/{id}", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "endsAt": "value", "startsAt": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/pass/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "endsAt": "value", "startsAt": "value" } response = requests.patch(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/pass/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PATCH", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "endsAt" => "value", "startsAt" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "endsAt": "value", "startsAt": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/pass/{id}") .method("PATCH", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/pass/{id}")!) request.httpMethod = "PATCH" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "endsAt": "value", "startsAt": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden` `404 Not Found` `415 Unsupported Media Type` `500 Internal Server Error` `502 Response` `503 '{ message: 'failed to update iglooKey token' }' if the upstream linking service was unreachable.` **Response body — `200`:** - **`id`** `string` - **`type`** `string`\ Token type, `TOKEN_TYPE_HYBRID`. - **`endsAt`** `string` - **`startsAt`** `string` - **`activationDeadline`** `string` **Response body — `400`:** - **`error`** `string` - **`message`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`missingIds`** `array of string` **Example response — `200`:** ```json { "activationDeadline": "2025-09-20T14:00:00Z", "endsAt": "2026-01-15T23:59:59Z", "id": "lnk_abc123def456ghi789jkl012mno345", "startsAt": "2025-09-20T12:00:00Z", "type": "TOKEN_TYPE_HYBRID" } ``` **Example response — `400`:** ```json { "error": "\"endsAt\" must be in the future" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "device not found" } ``` ### Revoke iglooKey Token `DELETE` `https://api.igloohome.co/home/pass/{id}` Revokes an existing iglooKey token. No request body. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Request Example ```bash curl -X DELETE "https://api.igloohome.co/home/pass/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/pass/{id}" req, _ := http.NewRequest("DELETE", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/pass/{id}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/pass/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.delete(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/pass/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/pass/{id}") .method("DELETE", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/pass/{id}")!) request.httpMethod = "DELETE" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `204 No Content` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden` `404 Not Found` `500 Internal Server Error` `502 '{ message: 'failed to revoke iglooKey token' }' if the upstream linking service returned a server error or an unrecognized status.` `503 '{ message: 'failed to revoke iglooKey token' }' if the upstream linking service was unreachable.` **Response body — `400`:** - **`error`** `string` - **`message`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`missingIds`** `array of string` **Example response — `400`:** ```json { "error": "\"endsAt\" must be in the future" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "device not found" } ``` ### FILE: home/api_devices Title: Devices Category: API Reference ---------------------------------------- # Devices API Reference API endpoints for managing devices. ## Get Devices `GET` `https://api.igloohome.co/home/devices` Returns a list of devices paired to the account. Devices include locks, as well as non-lock devices. |Device Type| |--| |Lock| |Keypad| |Bridge| |Fob| ### Query Parameters - **`limit`** `number`\ Number of objects to return, from 1 to 300. Defaults to 100. - Constraints: `>= 1`, `<= 300` - Default: `100` - Example: `30` - **`cursor`** `string`\ The pagination cursor. `cursor` is a string ID that marks your place in the list. For instance, if you make a list request and receive 100 objects, with `nextCursor` of `2sThyA1jii6WmTo3j`, your next call can include `cursor=2sThyA1jii6WmTo3j` in order to fetch the next page of the list. - **`sort`** `string`\ Order in which to return objects. Defaults to `desc`. - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-devices'` `500 Internal Server Error` **Response body — `200`:** - **`nextCursor`** `string`\ A string pointing to the next page of results. Empty string indicates you reached the end of the list - Example: `2sThyA1jii6WmTo3j` - **`payload`** `array of object`\ An array containing the return devices - **`id`** `string`\ Internal batch identifier of the device - Example: `2rqQx4Riasw3Rs6db01` - **`type`** `string`\ Device Type - Example: `Keypad` - **`deviceId`** `string`\ Unique Identifier of the device - Example: `EK1X013f300d` - **`deviceName`** `string`\ Name of the device - Example: `My keypad 1` - **`batteryLevel`** `integer`\ Battery level of the device - Example: `100` - **`pairedAt`** `string`\ Pairing date - Example: `2022-05-30T00:00:00+08:00` - **`homeId`** `array of string`\ Associated homes or properties. Populated as full property objects when you pass `expand=homeId`. - Example: `["2rqQx4Riasw3Rs6db"]` - **`linkedDevices`** `array of object`\ Device(s) linked to this device - Example: `[{"type":"Lock"},{"deviceId":"IGP102c1854a"}]` - **`id`** `string`\ Internal batch identifier of the linked device - **`type`** `string` - **`deviceId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "22j3230ids0idsa0d", "payload": [ { "batteryLevel": 100, "deviceId": "EK1X013f300d", "deviceName": "Keypad", "homeId": [ "2rqQx4Riasw3Rs6db" ], "linkedDevices": [ { "deviceId": "IGP102c1854a", "type": "Lock" } ], "pairedAt": "2022-06-30T17:00:00+08:00", "type": "Keypad" } ] } ``` **Example response — `400`:** ```json { "error": "'cursor' cannot be empty string" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ## Get Devices Supporting AlgoPIN `GET` `https://api.igloohome.co/home/devices/support-pins` Returns a list of devices paired to the account that support AlgoPIN codes. Same shape and filters as [Get Devices](/home/api_devices#get-devices), restricted to AlgoPIN-capable SKUs. ### Query Parameters - **`limit`** `number`\ Number of objects to return, from 1 to 300. Defaults to 100. - Constraints: `>= 1`, `<= 300` - Default: `100` - Example: `30` - **`cursor`** `string`\ The pagination cursor. `cursor` is a string ID that marks your place in the list. For instance, if you make a list request and receive 100 objects, with `nextCursor` of `2sThyA1jii6WmTo3j`, your next call can include `cursor=2sThyA1jii6WmTo3j` in order to fetch the next page of the list. - **`sort`** `string`\ Order in which to return objects. Defaults to `desc`. - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/support-pins" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request. Same validation as [Get Devices](/home/api_devices#get-devices).` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-devices'` `500 Internal Server Error` **Response body — `200`:** - **`nextCursor`** `string`\ A string pointing to the next page of results. Empty string indicates you reached the end of the list - **`payload`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`deviceName`** `string` - **`batteryLevel`** `integer` - **`pairedAt`** `string` - **`homeId`** `array of string` - **`linkedDevices`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "", "payload": [ { "batteryLevel": 87, "deviceId": "IGP102c1854a", "deviceName": "Front Door Lock", "homeId": [ "2rqQx4Riasw3Rs6db" ], "id": "2rqQx4Riasw3Rs6db01", "linkedDevices": [], "pairedAt": "2023-06-15T14:30:00+08:00", "type": "Lock" } ] } ``` **Example response — `400`:** ```json { "error": "'limit' must be between 1 to 300 inclusive" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ## Get Device Details `GET` `https://api.igloohome.co/home/devices/{deviceId}` Returns device details. `lockStatus` is only populated for a `Lock` device that is linked to a bridge. `admin_key`/`lockRef` are only populated when the corresponding query params are passed. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/{deviceId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-devices'` `404 '{ message: 'not found device' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard). '{ message: 'timezone is not found on property' }'` `500 Internal Server Error` **Response body — `200`:** - **`id`** `string`\ Internal batch identifier of the device - Example: `2rqQx4Riasw3Rs6db01` - **`type`** `string`\ Device Type - Example: `Keypad` - **`deviceId`** `string`\ Unique Identifier of the device - Example: `EK1X013f300d` - **`deviceName`** `string`\ Name of the device - Example: `My keypad 1` - **`batteryLevel`** `integer`\ Battery level of the device - Example: `100` - **`pairedAt`** `string`\ Pairing date - Example: `2022-05-30T00:00:00+08:00` - **`lockStatus`** `string`\ Current lock status. Only populated for a `Lock` device that is linked to a bridge. - Example: `LOCKED` - **`homeId`** `array of string`\ Associated home/property(s) - Example: `["2rqQx4Riasw3Rs6db"]` - **`linkedDevices`** `array of object`\ Device(s) linked to this device - Example: `[{"type":"Lock"},{"deviceId":"IGP102c1854a"}]` - **`type`** `string` - **`deviceId`** `string` - **`lastSync`** `string`\ Timestamp of the last activity sync from the device - **`admin_key`** `string`\ Obfuscated admin key material. Only returned for G2 devices when `admin_key=true`/`1` is passed. - **`lockRef`** `string`\ Internal database reference of the device. Only returned when `lock_id=true`/`1` is passed. **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "batteryLevel": 20, "deviceId": "EK1X013f300d", "deviceName": "My keypad 1", "homeId": [ "2rqQx4Riasw3Rs6db" ], "id": "2rqQx4Riasw3Rs6db01", "lastSync": "2022-05-30T00:00:00+08:00", "linkedDevices": [], "pairedAt": "2021-07-03T12:00:00+08:00", "type": "Keypad" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` ## Configure Device Details `PUT` `https://api.igloohome.co/home/devices/{deviceId}` Configures device settings. The API rejects `batteryLevel` with `400` on device SKUs that don't support battery-level reporting. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Body Parameters Content-Type: `application/json` - **`batteryLevel`** `integer`\ **batteryLevel** defines the level of battery detail reported by the lock. `null` is accepted and ignored: nothing is updated and `200` is returned. ### Request Example ```bash curl -X PUT "https://api.igloohome.co/home/devices/{deviceId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "batteryLevel": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}" payload := []byte(`{ "batteryLevel": "value" }`) req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}", { method: "PUT", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "batteryLevel": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "batteryLevel": "value" } response = requests.put(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PUT", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "batteryLevel" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "batteryLevel": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}") .method("PUT", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}")!) request.httpMethod = "PUT" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "batteryLevel": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 Settings updated.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/update-device'` `404 Not Found` `409 Conflict. The device's property record is missing a timezone (data integrity guard). '{ message: 'timezone is not found on property' }'` `415 Unsupported Media Type` `500 Internal Server Error` **Response body — `200`:** - **`message`** `string` - Example: `Successfully` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`error`** `string` - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "message": "Successfully" } ``` **Example response — `400`:** ```json { "error": "The BatteryLevel is too high! It must be at least 100" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` ## Get Device Activity Logs `GET` `https://api.igloohome.co/home/devices/{deviceId}/activity` Returns a paginated list of activity logs recorded for a device. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters - **`startDate`** `string`\ Only return activity logs on or after this date - **`endDate`** `string`\ Only return activity logs up to and including this date - **`locale`** `string`\ Locale for the human-readable `description` field. Must be one of the supported locale codes, otherwise the API returns `400`. Two of those codes (`ko`, `ru`) are accepted but not yet translated. They fall back to `en` instead of returning an error. - Default: `en` - **`limit`** `number`\ Number of objects to return, from 1 to 300. Defaults to 100. - Constraints: `>= 1`, `<= 300` - Default: `100` - Example: `30` - **`cursor`** `string`\ The pagination cursor. `cursor` is a string ID that marks your place in the list. For instance, if you make a list request and receive 100 objects, with `nextCursor` of `2sThyA1jii6WmTo3j`, your next call can include `cursor=2sThyA1jii6WmTo3j` in order to fetch the next page of the list. - **`sort`** `string`\ Order in which to return objects. Defaults to `desc`. - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/activity" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "startDate": "value", "endDate": "value", "locale": "value", "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request. Invalid 'locale'/'limit'/'cursor'/'sort'.` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-device-activity'` `404 '{ message: 'not found device' }' or '{ message: 'not found property' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard). '{ message: 'timezone is not found on property' }'` `500 Internal Server Error` **Response body — `200`:** - **`nextCursor`** `string`\ A string pointing to the next page of results. Empty string indicates you reached the end of the list - **`payload`** `array of object`\ An array containing activity log entries - **`activityType`** `string` - **`description`** `string`\ Human-readable, locale-translated description of the activity - **`activityTimeAt`** `string`\ Local time the activity occurred (alias of `localActionAt`) **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "nextCursor": "65a7d4c8b2c9f1e2d3a4b5c6", "payload": [ { "activityTimeAt": "2024-01-15T14:30:00+08:00", "activityType": "UNLOCK", "description": "Unlocked by keypad PIN" }, { "activityTimeAt": "2024-01-15T13:45:00+08:00", "activityType": "LOCK", "description": "Locked by bluetooth key" } ] } ``` **Example response — `400`:** ```json { "error": "'limit' must be between 1 to 300 inclusive" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` ## Push Device Activity Logs `POST` `https://api.igloohome.co/home/devices/{deviceId}/activity` Ingests raw activity logs synced off a device (e.g. by a bridge or app). Each item in `logsPayload` is a JSON-serialized log entry. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Body Parameters Content-Type: `application/json` - **`logsPayload`** `array of string` *required*\ Array of JSON-serialized log entry strings, each with `log` and `bluetoothDeviceName` fields ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/activity" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "logsPayload": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/activity" payload := []byte(`{ "logsPayload": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/activity", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "logsPayload": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/activity" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "logsPayload": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/activity", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "logsPayload" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "logsPayload": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/activity") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/activity")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "logsPayload": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 '{ error: "\"logsPayload\" is required" }'` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/store-device-activity'` `404 '{ message: 'not found device' }' or '{ message: 'not found property' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard). '{ message: 'timezone is not found on property' }'` `415 Unsupported Media Type` `500 Internal Server Error` **Response body — `200`:** - **`message`** `string` - Example: `Successfully.` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "message": "Successfully." } ``` **Example response — `400`:** ```json { "error": "\"logsPayload\" is required" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` ## Get Master PIN `GET` `https://api.igloohome.co/home/devices/{deviceId}/masterpin` Returns the master pin of the given device. Supports `lock` and `keypad` devices only. |Device Type| |--| |Lock| |Keypad| ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/{deviceId}/masterpin" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/masterpin" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/masterpin", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/masterpin" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/masterpin", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/masterpin") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/masterpin")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-master-pin'` `404 '{ message: 'not found device' }' or '{ message: 'not found property' }'` `409 Conflict. The device's property record is missing a timezone (data integrity guard). '{ message: 'timezone is not found on property' }'` `500 Internal Server Error` **Response body — `200`:** - **`pin`** `string`\ Master pin - Example: `49667198` **Response body — `402`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "pin": "49667198" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `404`:** ```json { "message": "not found device" } ``` **Example response — `409`:** ```json { "message": "timezone is not found on property" } ``` ### FILE: home/api_jobs Title: Jobs Category: API Reference ---------------------------------------- # Jobs API Reference API endpoints for managing jobs. ## Lock Jobs Bridge Proxied Jobs send commands to smart locks through a Bridge device. The Bridge acts as an intermediary that communicates with locks via Bluetooth while your application communicates with the Bridge via the API. Send these jobs with [Create a Bridge Proxied Job](#create-a-bridge-proxied-job). ### Job Creation Create a job by making a `POST` request to: `/devices/{deviceId}/jobs/bridges/{bridgeId}` **Request Parameters** | Parameter | Type | Description | | --- | --- | --- | | jobType | number | Command type identifier (see Job Types) | | jobData | object | Command-specific parameters | **Response** ```json { "jobId": "string" } ``` **Job Types** | Job Type | ID | | --- | --- | | Lock | 1 | | Unlock | 2 | | Create Custom PIN code | 4 | | Delete PIN code | 5 | | Get Battery Level | 9 | | Get Device Status | 10 | | Get Activity Logs | 15 | ### Command Details #### Lock Locks the device and optionally synchronizes its internal clock. | Parameter | Type | Description | Required | | --- | --- | --- | --- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | ```json { "jobType": 1, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` #### Unlock Unlocks the device and optionally synchronizes its internal clock. | Parameter | Type | Description | Required | | --- | --- | --- | --- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | ```json { "jobType": 2, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` #### Create Custom PIN Code Creates a PIN code with specific access permissions and time restrictions. Devices/locks supporting PIN are listed in [Supported Devices](/home/supported-devices). | Parameter | Type | Description | Required | | --- | --- | --- | --- | | accessName | string | Display name for the PIN code, up to 60 characters | Yes | | pin | string | 4-6 digit PIN code | Yes | | pinType | number | PIN type (see table below) | Yes | | startDate | string | When PIN becomes active (ISO_DATE format) | Yes | | endDate | string | When PIN expires (ISO_DATE format) | Duration only | **PIN Types** | Type | ID | Description | | --- | --- | --- | | One-Time | 1 | Valid for 24 hours from start date | | Permanent | 2 | Valid indefinitely from start date | | Duration | 4 | Valid between start and end dates | ```json { "jobType": 4, "jobData": { "accessName": "Guest PIN", "pin": "123456", "pinType": 4, "startDate": "2024-01-01T12:00:00+00:00", "endDate": "2024-01-01T16:00:00+00:00" } } ``` One-Time PIN (24-hour validity): ```json { "jobType": 4, "jobData": { "accessName": "OTP PIN", "pin": "123456", "pinType": 1, "startDate": "2024-01-01T00:00:00+00:00" } } ``` #### Delete PIN Code Removes a PIN code from the lock. An unactivated AlgoPIN with a start date more than 256 hours after its creation cannot be deleted. | Parameter | Type | Description | Required | | --- | --- | --- | --- | | pin | string | PIN code to be deleted, 4-9 digits | Yes | ```json { "jobType": 5, "jobData": { "pin": "123456" } } ``` #### Get Battery Level Retrieves the lock's current battery level as a percentage. ```json { "jobType": 9 } ``` #### Get Device Status Queries the lock's current operational status. ```json { "jobType": 10 } ``` #### Get Activity Logs Downloads activity logs from the lock. Optionally synchronizes the lock's clock. Activity logs are deleted from the lock after retrieval. | Parameter | Type | Description | Required | | --- | --- | --- | --- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | ```json { "jobType": 15, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` ### Error Status Codes | Status Code | Description | Applies To | | --- | --- | --- | | 400 | Request validation failed, e.g. `job type not supported. see documentation for supported job type(s)` or `pin type not supported. see documentation for supported pin type(s)` | All | | 401 | Missing, invalid or expired access token | All | | 402 | The igloohome API trial has ended, or the billing grace period has expired | All | | 403 | The token lacks the scope for this job type | All | | 404 | Bridge or device not found | All | | 406 | `PIN (123456) is already taken. Please try a different pin` | Create Custom PIN code | | 406 | `Lock is not linked to bridge` | All | | 406 | `Unable to contact bridge as it appears to be offline` | All | | 415 | Missing `Content-Type: application/json` | All | | 500 | Unexpected server error | All | ## Bridge Jobs These jobs target the Bridge device itself, not a lock. Send these jobs with [Create a Bridge Targeted Job](#create-a-bridge-targeted-job). ### Job Creation Create a job by making a `POST` request to: `/devices/{bridgeId}/jobs` `bridgeId` is the Bridge's device ID. | Parameter | Type | | --- | --- | | jobType | integer, see Job Types for a full list | ```json { "jobType": 10 } ``` **Response** ```json { "jobId": "string" } ``` **Job Types** | Job Type | Number | | --- | --- | | Get Device Status | 10 | ### Job Details #### Get Device Status (Bridge Diagnostic) ```json { "jobType": 10 } ``` Once the job completes, you receive a webhook with the result. See [Get Device Status](/home/bridge-events#get-device-status) for the payload. **Rate limit:** one Get Device Status job per bridge every 5 minutes per user. Further requests return `429` with a `Retry-After` header (seconds). ### Error Status Codes | Status Code | Message (API Response) | Description | Applies To | | --- | --- | --- | --- | | 400 | Bad Request | The request was unacceptable, often due to a missing or invalid parameter (e.g., `` is required). | All | | 401 | Unauthorized | No valid API key or access token was provided in the request header. | All | | 402 | Payment Required (Various Messages) | The API trial has ended or the payment method was declined. Please verify billing details. | All | | 403 | Forbidden | The authenticated user does not have the necessary permissions (scopes) for this resource. | All | | 404 | Not Found | The requested resource does not exist or the endpoint URL is incorrect. | All | | 405 | Method Not Allowed | The HTTP method used is not supported by this endpoint. | All | | 406 | Unable to contact bridge as it appears to be offline | Unable to contact the bridge. Ensure the physical hardware is powered on and connected. | All | | 415 | Unsupported Media Type | The Content-Type header must be set to application/json. | N/A | | 429 | Too many requests. Please try again after X seconds. | Rate limit exceeded: one job per bridge every 5 minutes per user. Wait for the `Retry-After` seconds. | All | | 500 | Internal Server Error | An unexpected error occurred on our end. Please retry later or contact support. | All | ## Create a Bridge Targeted Job `POST` `https://api.igloohome.co/home/devices/{bridgeId}/jobs` Creates a job that targets a Bridge device directly, not a lock via a bridge. Supports only `jobType` 10 (Get Device Status). Rate limit: one `GET_DEVICE_STATUS` job per bridge every 5 minutes per user (except for internal igloohome accounts). ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | bridgeId | string | Yes | ### Body Parameters Content-Type: `application/json` - **`jobType`** `integer` *required*\ 10 = Get Device Status. The only supported value. - Allowed values: `10` ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{bridgeId}/jobs" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "jobType": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{bridgeId}/jobs" payload := []byte(`{ "jobType": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{bridgeId}/jobs", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "jobType": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{bridgeId}/jobs" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "jobType": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{bridgeId}/jobs", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "jobType" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "jobType": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{bridgeId}/jobs") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{bridgeId}/jobs")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "jobType": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-device-status-bridge-proxied-job'` `404 Bridge Not Found` `406 '{ error: 'Unable to contact bridge as it appears to be offline' }'` `415 Unsupported Media Type` `429 Too Many Requests. '{ error: 'Too many requests. Please try again after N seconds.' }'. The response includes a 'Retry-After' header (seconds).` `500 Internal Server Error` **Response body — `200`:** - **`jobId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `406`:** - **`error`** `string` **Response body — `429`:** - **`error`** `string` **Example response — `200`:** ```json { "jobId": "6345191a82bcfc12a921bdb6" } ``` **Example response — `400`:** ```json { "error": "job type not supported. see documentation for supported job type(s)" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `406`:** ```json { "error": "Unable to contact bridge as it appears to be offline" } ``` **Example response — `429`:** ```json { "error": "Too many requests. Please try again after 45 seconds." } ``` ## Create a Bridge Proxied Job `POST` `https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}` Creates a job for a lock via a Bridge device. #### Supported Job Types |Job Type|Number| |--|--| |Lock|1| |Unlock|2| |Create Custom PIN code (duration) |4| |Delete PIN code|5| |Get Battery Level|9| |Get Device Status|10| |Get Activity Logs|15| `jobData` is only required for `jobType` 4 (Create Custom PIN code) and 5 (Delete PIN code). |jobType|Required jobData fields| |--|--| |4 (Create Custom PIN code)|`accessName`, `pin`, `pinType`, `startDate`, and `endDate` (only when `pinType` is 4/DURATION)| |5 (Delete PIN code)|`pin` only| ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | | bridgeId | string | Yes | ### Body Parameters Content-Type: `application/json` - **`jobType`** `integer` *required*\ See the job type table above - Allowed values: `1`, `2`, `4`, `5`, `9`, `10`, `15` - **`jobData`** `object`\ Shape depends on `jobType`. See the table above. Omit for jobTypes other than 4 and 5. - **`accessName`** `string`\ Required when `jobType` is 4 (Create Custom PIN code). - **`pinType`** `integer`\ 1 = One-Time, 2 = Permanent, 4 = Duration. Required when `jobType` is 4. - Allowed values: `1`, `2`, `4` - **`pin`** `string`\ 4-6 digits when `jobType` is 4 (Create Custom PIN code); 4-9 digits when `jobType` is 5 (Delete PIN code). - **`startDate`** `string`\ Required when `jobType` is 4. Must be in format `YYYY-MM-DDTHH:mm:ss+hh:mm`. - **`endDate`** `string`\ Required when `jobType` is 4 and `pinType` is 4 (Duration). Must be in format `YYYY-MM-DDTHH:mm:ss+hh:mm` and after `startDate`. ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "jobData": "value", "jobType": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}" payload := []byte(`{ "jobData": "value", "jobType": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "jobData": "value", "jobType": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "jobData": "value", "jobType": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "jobData" => "value", "jobType" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "jobData": "value", "jobType": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "jobData": "value", "jobType": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 Job Created` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain the scope for the given 'jobType' (see table below)` `404 Bridge or Device Not Found` `406 Not Acceptable` `415 Unsupported Media Type` `500 Internal Server Error` **Response body — `200`:** - **`jobId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Response body — `406`:** - **`error`** `string` **Example response — `200`:** ```json { "jobId": "6345191a82bcfc12a921bdb6" } ``` **Example response — `400`:** ```json { "error": "job type not supported. see documentation for supported job type(s)" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` **Example response — `406`:** ```json { "error": "Unable to contact bridge as it appears to be offline" } ``` ## Get Job Status `GET` `https://api.igloohome.co/home/jobs/{jobId}` Returns the status of a job you created earlier. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | jobId | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/jobs/{jobId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/jobs/{jobId}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/jobs/{jobId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/jobs/{jobId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/jobs/{jobId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/jobs/{jobId}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/jobs/{jobId}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-job-status'` `404 Not Found` `422 'jobId' is not a valid job ID (24-character hex)` `500 Internal Server Error` **Response body — `200`:** - **`jobId`** `string` - **`expiryDate`** `string` - **`completed`** `boolean` - **`jobType`** `string` - **`jobResponse`** `object` - **`jobStatus`** `integer`\ 0 = completed, 1 = pending, 2 = expired - Allowed values: `0`, `1`, `2` - **`opResult`** `object` - **`result`** `integer` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "completed": true, "expiryDate": "2025-01-01T00:02:00.000Z", "jobId": "6345191a82bcfc12a921bdb6", "jobResponse": { "jobStatus": 0, "opResult": { "result": 0 } }, "jobType": "UNLOCK" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ### FILE: home/api_properties Title: Properties Category: API Reference ---------------------------------------- # Properties API Reference API endpoints for managing properties. ## Get Properties `GET` `https://api.igloohome.co/home/properties` Returns list of properties on the account ### Query Parameters - **`limit`** `number`\ Number of objects to return, from 1 to 300. Defaults to 100. - Constraints: `>= 1`, `<= 300` - Default: `100` - Example: `30` - **`cursor`** `string`\ The pagination cursor. `cursor` is a string ID that marks your place in the list. For instance, if you make a list request and receive 100 objects, with `nextCursor` of `2sThyA1jii6WmTo3j`, your next call can include `cursor=2sThyA1jii6WmTo3j` in order to fetch the next page of the list. - **`sort`** `string`\ Order in which to return objects. Defaults to `desc`. - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/properties" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden. accessToken scope must contain 'igloohomeapi/get-properties'` `500 Internal Server Error` **Response body — `200`:** - **`nextCursor`** `string`\ A string pointing to the next page of results. Empty string indicates you reached the end of the list - Example: `2sFZqWxJuynYnwLHg` - **`payload`** `array of object`\ An array containing the return properties - **`name`** `string`\ Name of the property - Example: `Singapore HQ` - **`id`** `string`\ Unique Identifier of the property - Example: `2sFZqWxJuynYnwLHg` - **`timezone`** `string`\ Property timezone - Example: `Asia/Singapore` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "2rqQx4Riasw3Rs6db", "payload": [ { "id": "2sFZqWxJuynYnwLHg", "name": "Singapore HQ", "timezone": "Asia/Singapore" }, { "id": "2rqQx4Riasw3Rs6db", "name": "Igloocompany HQ", "timezone": "Asia/Singapore" } ] } ``` **Example response — `400`:** ```json { "error": "'limit' must be between 1 to 300 inclusive" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ## Get Property Detail `GET` `https://api.igloohome.co/home/properties/{propertyId}` Returns info on a property ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | propertyId | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/properties/{propertyId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/properties/{propertyId}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/properties/{propertyId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/properties/{propertyId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/properties/{propertyId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/properties/{propertyId}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/properties/{propertyId}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden. accessToken scope must contain 'igloohomeapi/get-properties'` `404 Property Not Found` `500 Internal Server Error` **Response body — `200`:** - **`payload`** `object` - **`name`** `string`\ Name of the property - Example: `Singapore HQ` - **`id`** `string`\ Unique Identifier of the property - Example: `2sFZqWxJuynYnwLHg` - **`timezone`** `string`\ Property timezone - Example: `Asia/Singapore` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "payload": { "id": "2sFZqWxJuynYnwLHg", "name": "Singapore HQ", "timezone": "Asia/Singapore" } } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ## Get Devices by Property ID `GET` `https://api.igloohome.co/home/properties/{propertyId}/devices` Returns a list of devices of given property. Devices include locks, as well as non-lock devices. |Device Type| |--| |Lock| |Keypad| |Bridge| |Fob| ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | propertyId | string | Yes | ### Query Parameters - **`cursor`** `string`\ The pagination cursor. `cursor` is a string ID that marks your place in the list. For instance, if you make a list request and receive 100 objects, with `nextCursor` of `2sThyA1jii6WmTo3j`, your next call can include `cursor=2sThyA1jii6WmTo3j` in order to fetch the next page of the list. - **`limit`** `number`\ Number of objects to return, from 1 to 300. Defaults to 100. - Constraints: `>= 1`, `<= 300` - Default: `100` - Example: `30` - **`sort`** `string`\ Order in which to return objects. Defaults to `desc`. - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/properties/{propertyId}/devices" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "cursor": "value", "limit": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-devices'` `500 Internal Server Error` **Response body — `200`:** - **`nextCursor`** `string`\ A string pointing to the next page of results. Empty string indicates you reached the end of the list - Example: `2sThyA1jii6WmTo3j` - **`payload`** `array of object`\ An array containing the return devices - **`id`** `string`\ Internal batch identifier of the device - Example: `2rqQx4Riasw3Rs6db01` - **`type`** `string`\ Device Type - Example: `Keypad` - **`deviceId`** `string`\ Unique Identifier of the device - Example: `EK1X013f300d` - **`deviceName`** `string`\ Name of the device - Example: `My keypad 1` - **`batteryLevel`** `integer`\ Battery level of the device - **`pairedAt`** `string`\ Pairing date - Example: `2022-05-30T00:00:00+08:00` - **`homeId`** `array of string`\ Associated home/property(s) - Example: `["2rqQx4Riasw3Rs6db"]` - **`linkedDevices`** `array of object`\ Device(s) linked to this device - Example: `[{"type":"Lock"},{"deviceId":"IGP102c1854a"}]` - **`id`** `string`\ Internal batch identifier of the linked device - **`type`** `string` - **`deviceId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "22j3230ids0idsa0d", "payload": [ { "deviceId": "EK1X013f300d", "deviceName": "Keypad", "homeId": [ "2rqQx4Riasw3Rs6db" ], "linkedDevices": [ { "deviceId": "IGP102c1854a", "type": "Lock" } ], "pairedAt": "2022-06-30T17:00:00+08:00", "type": "Keypad" } ] } ``` **Example response — `400`:** ```json { "error": "'limit' must be between 1 to 300 inclusive" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ### FILE: home/api_server_health Title: Server Health Category: API Reference ---------------------------------------- # Server Health API Reference API endpoints for managing server health. ## Get Server Health `GET` `https://api.igloohome.co/home/server-health` Returns the caller's OAuth client/user billing status. Use it for integration health checks. Requires a valid Bearer token but no specific scope. The endpoint runs the same billing check as every other authenticated endpoint, so a lapsed trial/billing status returns `402`/`403` before the API returns the health payload. ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/server-health" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/server-health" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/home/server-health", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/server-health" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/home/server-health", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/home/server-health") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/home/server-health")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden. The authenticated user has revoked access, or their account was removed.` `500 Internal Server Error` **Response body — `200`:** - **`userRef`** `string` - **`clientId`** `string` - **`clientName`** `string` - **`status`** `string` - **`endsAt`** `string` - **`isAdminLogin`** `boolean` - **`flow`** `string`\ OAuth client flow (e.g. `code`, `client_credentials`) - **`scopes`** `array of string` - **`tier`** `string` - **`codeUserRef`** `string`\ Only present when `flow` is `code` **Response body — `402`:** - **`error`** `string` **Example response — `200`:** ```json { "clientId": "7k8m9n0p1q2r3s4t5u6v7w8x", "clientName": "igloohome-partners-app", "endsAt": "2026-09-08T00:00:00Z", "flow": "client_credentials", "isAdminLogin": false, "scopes": [ "igloohomeapi/get-devices" ], "status": "active", "tier": "paid", "userRef": "user_abc123def456" } ``` **Example response — `402`:** ```json { "error": "30 days igloohome API trial has ended" } ``` ### FILE: works/api_access Title: Access Category: API Reference ---------------------------------------- # Access API Reference API endpoints for managing access. ## AlgoPIN ### Create a Daily AlgoPIN `POST` `https://api.igloohome.co/works/devices/{id}/algopin/daily` See `POST /devices/{id}/algopin/permanent`. Identical shape, different scope. `endDate` is required for this PIN type (forbidden for permanent/onetime), and `variance` range is 1–3 (not 1–10). #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`accessName`** `string` *required* - **`departmentId`** `string`\ Required unless the account has a single department. - **`startDate`** `string` *required*\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Must be now or after. - **`endDate`** `string`\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Required for daily/hourly, forbidden for onetime/permanent. Must be after `startDate`. Hourly: at most 672 hours after `startDate`. - **`variance`** `integer` *required*\ Daily/hourly: 1–3. Onetime/permanent: 1–10. Values 6–10 require newer lock firmware (some older locks support them after a firmware update); otherwise use 1–5. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/daily" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/algopin/daily" payload := []byte(`{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/algopin/daily", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/algopin/daily" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/algopin/daily", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "departmentId" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/algopin/daily") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/algopin/daily")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. If a matching PIN already exists for this lock, type, and window with the same 'variance', the API returns the same success response without creating a new PIN. This idempotent behavior is not an error.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Missing a required scope: AlgoPIN endpoints need both the general AlgoPIN scope and the scope for the specific PIN type (see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`pin`** `string` - **`pinId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "string", "pinId": "string" } ``` **Example response — `400`:** ```json { "error": "departmentId is missing" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `409`:** ```json { "error": "department not found" } ``` ### Create an Hourly AlgoPIN `POST` `https://api.igloohome.co/works/devices/{id}/algopin/hourly` See `POST /devices/{id}/algopin/permanent`. Identical shape, different scope. `endDate` is required for this PIN type (forbidden for permanent/onetime), and `variance` range is 1–3 (not 1–10). #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`accessName`** `string` *required* - **`departmentId`** `string`\ Required unless the account has a single department. - **`startDate`** `string` *required*\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Must be now or after. - **`endDate`** `string`\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Required for daily/hourly, forbidden for onetime/permanent. Must be after `startDate`. Hourly: at most 672 hours after `startDate`. - **`variance`** `integer` *required*\ Daily/hourly: 1–3. Onetime/permanent: 1–10. Values 6–10 require newer lock firmware (some older locks support them after a firmware update); otherwise use 1–5. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/hourly" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/algopin/hourly" payload := []byte(`{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/algopin/hourly", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/algopin/hourly" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/algopin/hourly", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "departmentId" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/algopin/hourly") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/algopin/hourly")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. If a matching PIN already exists for this lock, type, and window with the same 'variance', the API returns the same success response without creating a new PIN. This idempotent behavior is not an error.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Missing a required scope: AlgoPIN endpoints need both the general AlgoPIN scope and the scope for the specific PIN type (see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`pin`** `string` - **`pinId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "string", "pinId": "string" } ``` **Example response — `400`:** ```json { "error": "departmentId is missing" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `409`:** ```json { "error": "department not found" } ``` ### Create a One-Time AlgoPIN `POST` `https://api.igloohome.co/works/devices/{id}/algopin/onetime` See `POST /devices/{id}/algopin/permanent`. Identical shape, different scope. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`accessName`** `string` *required* - **`departmentId`** `string`\ Required unless the account has a single department. - **`startDate`** `string` *required*\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Must be now or after. - **`endDate`** `string`\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Required for daily/hourly, forbidden for onetime/permanent. Must be after `startDate`. Hourly: at most 672 hours after `startDate`. - **`variance`** `integer` *required*\ Daily/hourly: 1–3. Onetime/permanent: 1–10. Values 6–10 require newer lock firmware (some older locks support them after a firmware update); otherwise use 1–5. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/onetime" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/algopin/onetime" payload := []byte(`{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/algopin/onetime", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/algopin/onetime" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/algopin/onetime", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "departmentId" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/algopin/onetime") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/algopin/onetime")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. If a matching PIN already exists for this lock, type, and window with the same 'variance', the API returns the same success response without creating a new PIN. This idempotent behavior is not an error.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Missing a required scope: AlgoPIN endpoints need both the general AlgoPIN scope and the scope for the specific PIN type (see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`pin`** `string` - **`pinId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "string", "pinId": "string" } ``` **Example response — `400`:** ```json { "error": "departmentId is missing" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `409`:** ```json { "error": "department not found" } ``` ### Create a Passage AlgoPIN `POST` `https://api.igloohome.co/works/devices/{id}/algopin/passage` Allows unlock with AlgoPIN and keeps the lock unlocked for the duration given in the request body. Supports newer devices with the latest firmware only. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`variance`** `integer` *required* - Allowed values: `1` - **`startDate`** `string` *required*\ Date and time when the permission starts. Must be in format `YYYY-MM-DDTHH:00:00+hh:mm` (hour resolution only). The PIN is valid from `startDate` for `durationHours` hours. - Example: `2022-01-01T12:00:00+08:00` - **`durationHours`** `integer` *required*\ Must be between 1 and 12 hours - **`accessName`** `string` *required*\ Name or short description of the access - Example: `Maintenance guy` - **`departmentId`** `string`\ Required unless the account has a single department. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/passage" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/algopin/passage" payload := []byte(`{ "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/algopin/passage", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/algopin/passage" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/algopin/passage", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "departmentId" => "value", "durationHours" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/algopin/passage") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/algopin/passage")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "departmentId": "value", "durationHours": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. If a matching PIN already exists for this lock, type, and window with the same 'variance', the API returns the same success response without creating a new PIN. This idempotent behavior is not an error.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Missing a required scope: AlgoPIN endpoints need both the general AlgoPIN scope and the scope for the specific PIN type (see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`pin`** `string` - **`pinId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "string", "pinId": "string" } ``` **Example response — `400`:** ```json { "error": "departmentId is missing" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `409`:** ```json { "error": "department not found" } ``` ### Create a Permanent AlgoPIN `POST` `https://api.igloohome.co/works/devices/{id}/algopin/permanent` Request and response shape match the other AlgoPIN endpoints (onetime/daily/hourly). Requires the `algopin-permanent` scope. A different AlgoPIN scope (e.g. `algopin-daily`) is not sufficient and returns a 403. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`accessName`** `string` *required* - **`departmentId`** `string`\ Required unless the account has a single department. - **`startDate`** `string` *required*\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Must be now or after. - **`endDate`** `string`\ Format `YYYY-MM-DDTHH:00:00+hh:mm`. Required for daily/hourly, forbidden for onetime/permanent. Must be after `startDate`. Hourly: at most 672 hours after `startDate`. - **`variance`** `integer` *required*\ Daily/hourly: 1–3. Onetime/permanent: 1–10. Values 6–10 require newer lock firmware (some older locks support them after a firmware update); otherwise use 1–5. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/permanent" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/algopin/permanent" payload := []byte(`{ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/algopin/permanent", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/algopin/permanent" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/algopin/permanent", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "accessName" => "value", "departmentId" => "value", "endDate" => "value", "startDate" => "value", "variance" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/algopin/permanent") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/algopin/permanent")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "accessName": "value", "departmentId": "value", "endDate": "value", "startDate": "value", "variance": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. If a matching PIN already exists for this lock, type, and window with the same 'variance', the API returns the same success response without creating a new PIN. This idempotent behavior is not an error.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Missing a required scope: AlgoPIN endpoints need both the general AlgoPIN scope and the scope for the specific PIN type (see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`pin`** `string` - **`pinId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Example response — `200`:** ```json { "pin": "string", "pinId": "string" } ``` **Example response — `400`:** ```json { "error": "departmentId is missing" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `409`:** ```json { "error": "department not found" } ``` ## eKey ### Grant eKey Access `POST` `https://api.igloohome.co/works/devices/{id}/access/grant` Creates an eKey access record for the recipient and logs it in device activity and access history. All error responses on this endpoint use a `{message, code}` body shape. See the 404 response below for the specific not-found causes. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`recipientId`** `string` *required* - **`startDateTime`** `string` *required*\ Must include an explicit timezone offset ('Z' or '+08:00'). - **`endDateTime`** `string` *required*\ Must be after startDateTime and after the current time. Same explicit-timezone-offset requirement. - **`permissions`** `array of string` *required*\ Narrower allow-list than POST /ekey's 56-value list, even though both mint an eKey. - **`departmentRef`** `string` - **`lang`** `string` #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/access/grant" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/access/grant" payload := []byte(`{ "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/access/grant", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/access/grant" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/access/grant", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "departmentRef" => "value", "endDateTime" => "value", "lang" => "value", "permissions" => "value", "recipientId" => "value", "startDateTime" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/access/grant") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/access/grant")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "departmentRef": "value", "endDateTime": "value", "lang": "value", "permissions": "value", "recipientId": "value", "startDateTime": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `201 Created. If activity logging fails after the API saves the access record, the API still returns 201, but with a different response body (no 'code' field) containing an error message.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 RECIPIENT_NOT_FOUND / LOCK_NOT_FOUND / DEPARTMENT_NOT_FOUND.` `409 CONFIGURATION_LOCK_ISSUE / FAILED_GENERATE_EKEY.` `415 Missing 'Content-Type: application/json'.` `422 NON_LOCK_DEVICE_NOT_ALLOWED. Returned when the target device is not a Lock.` `500 FLAT_ORG_DEPT_NOT_FOUND / INTERNAL_SERVER_ERROR.` **Response body — `201`:** - **`message`** `string` - **`accesses`** `array of object` - **`_id`** `string` - **`userRef`** `string` - **`recipient`** `string` - **`lockRef`** `string` - **`accessType`** `string` - Allowed values: `ekey` - **`via`** `string` - **`startDateTime`** `string` - **`endDateTime`** `string` - **`permissions`** `array of string` - **`departmentRef`** `string` - **`description`** `string` - **`bluetoothGuestKey`** `string` - **`keyId`** `number` - **`accessRight`** `string` **Response body — `400`:** - **`message`** `string` - **`code`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`code`** `string` **Response body — `409`:** - **`message`** `string` - **`code`** `string` **Response body — `422`:** - **`message`** `string` - **`code`** `string` **Response body — `500`:** - **`message`** `string` - **`code`** `string` **Example response — `201`:** ```json { "accesses": [ { "_id": "string", "accessRight": "string", "accessType": "string", "bluetoothGuestKey": "string", "departmentRef": "string", "description": "string", "endDateTime": "string", "keyId": 0, "lockRef": "string", "permissions": [ "string" ], "recipient": "string", "startDateTime": "string", "userRef": "string", "via": "string" } ], "message": "string" } ``` **Example response — `400`:** ```json { "code": "string", "message": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "code": "string", "message": "string" } ``` **Example response — `409`:** ```json { "code": "string", "message": "string" } ``` **Example response — `422`:** ```json { "code": "string", "message": "string" } ``` **Example response — `500`:** ```json { "code": "string", "message": "string" } ``` ### Revoke eKey Access `POST` `https://api.igloohome.co/works/devices/{id}/access/revoke` Revokes all matching access records for this recipient and permanently deletes their associated key material. You cannot undo this action. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`recipientId`** `string` *required* #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/access/revoke" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "recipientId": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/access/revoke" payload := []byte(`{ "recipientId": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/access/revoke", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "recipientId": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/access/revoke" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "recipientId": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/access/revoke", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "recipientId" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "recipientId": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/access/revoke") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/access/revoke")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "recipientId": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 RECIPIENT_NOT_FOUND / LOCK_NOT_FOUND / NO_ACTIVE_ACCESS ("Ekey already revoked or not found").` `415 Missing 'Content-Type: application/json'.` `500 INTERNAL_SERVER_ERROR. Unlike grant, revoke has no tolerant fallback if activity/audit-trail logging fails. Any failure here returns a 500.` **Response body — `200`:** - **`message`** `string` - Allowed values: `Access revoked successfully` - **`revokedCount`** `integer` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`code`** `string` **Response body — `500`:** - **`message`** `string` - **`code`** `string` **Example response — `200`:** ```json { "message": "string", "revokedCount": 0 } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "code": "string", "message": "string" } ``` **Example response — `500`:** ```json { "code": "string", "message": "string" } ``` ### Generate eKey `POST` `https://api.igloohome.co/works/devices/{id}/ekey` Returns an eKey without creating an access record. To create a tracked access record, use Grant eKey Access. Error responses are plain text, not JSON. Only a successful response returns a JSON body. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`startDate`** `string` *required*\ Format `YYYY-MM-DDTHH:mm:00Z` (minute resolution). - **`endDate`** `string` *required*\ Must be after `startDate`. Same format. - **`permissions`** `array of string` *required*\ Each must be a valid permission type (56 supported values). #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/ekey" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "endDate": "value", "permissions": "value", "startDate": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/ekey" payload := []byte(`{ "endDate": "value", "permissions": "value", "startDate": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/ekey", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "endDate": "value", "permissions": "value", "startDate": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/ekey" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "endDate": "value", "permissions": "value", "startDate": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/ekey", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "endDate" => "value", "permissions" => "value", "startDate" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "endDate": "value", "permissions": "value", "startDate": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/ekey") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/ekey")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "endDate": "value", "permissions": "value", "startDate": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `409 Plain-text body: '"failed.generate.ekey"' (key derivation failed).` `415 Missing 'Content-Type: application/json'.` `500 Plain-text body. Uncaught exception.` **Response body — `200`:** - **`keyId`** `number` - **`bluetoothGuestKey`** `string`\ Base64-encoded obfuscated blob. **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "bluetoothGuestKey": "string", "keyId": 0 } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## Access Records ### List Device Access `GET` `https://api.igloohome.co/works/devices/{id}/access` A malformed query parameter returns a 400, even if authentication is missing or invalid. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Query Parameters - **`accessType`** `string`\ Comma-separated list from `pin,ekey,nfc,fingerprint`. Invalid entries → 400 `{error: "valid 'accessType' - pin,ekey,nfc,fingerprint"}`. - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` #### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/access" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "accessType": "value", "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/access?accessType=value&limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Device not found, or (Lock/Keypad only) Property not found.` `409 Property timezone is missing.` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`name`** `string` - **`accessType`** `string` - Allowed values: `pin`, `ekey`, `nfc`, `fingerprint` - **`bluetoothDeviceName`** `string` - **`isCustomPin`** `boolean`\ Present only if accessType===pin. - **`pin`** `string`\ Present only if accessType===pin. - **`pinType`** `string`\ Present only if accessType===pin. The API stores and reports AlgoPIN daily/hourly variants as `duration`. - Allowed values: `otp`, `duration`, `permanent` - **`startDateTime`** `string` - **`endDateTime`** `string` - **`recipient`** `object`\ Present only if `accessType` is `ekey`. - **`name`** `string` - **`email`** `string` - **`createdAt`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `409`:** - **`message`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "accessType": "pin", "bluetoothDeviceName": "IGH-LK-3F9A21", "createdAt": "2026-09-13T09:30:00+08:00", "endDateTime": "2026-09-14T18:00:00+08:00", "id": "556e1b3a8c2d4e5f6a7b8c8e", "isCustomPin": true, "name": "Temp PIN - Maintenance", "pin": "2847", "pinType": "duration", "startDateTime": "2026-09-13T10:00:00+08:00" } ] } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "message": "string" } ``` **Example response — `409`:** ```json { "message": "string" } ``` ### FILE: works/api_account Title: Account Category: API Reference ---------------------------------------- # Account API Reference API endpoints for managing account. ## Get Account `GET` `https://api.igloohome.co/works/account` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/account" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/account" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/account", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/account" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/account", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/account") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/account")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`payload`** `object` - **`_id`** `string` - **`companyId`** `string` - **`companyName`** `string` - **`isFlatOrg`** `boolean`\ A single-department ("flat org") account has no department structure. Its devices and users all belong to one implicit department. When `true`, `departmentId`/`departmentRef` fields on other endpoints become optional or not applicable. When `false`, an explicit department is required. **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "payload": { "_id": "string", "companyId": "string", "companyName": "string", "isFlatOrg": false } } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### FILE: works/api_activity Title: Activity Category: API Reference ---------------------------------------- # Activity API Reference API endpoints for managing activity. ## List Device Activity `GET` `https://api.igloohome.co/works/devices/{id}/activity` Returns the device's activity logs translated into readable events. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters - **`startDate`** `string`\ Format `YYYY-MM-DD` (date only, no time component). - **`endDate`** `string`\ Format `YYYY-MM-DD` (date only, no time component). Must be on or after startDate. - **`locale`** `string`\ Language to translate activity messages into. - Default: `en` - Allowed values: `en`, `fr`, `id`, `ko`, `zh`, `ru` - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/activity" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "startDate": "value", "endDate": "value", "locale": "value", "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/activity?startDate=value&endDate=value&locale=value&limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object`\ Translated activity records, enriched with related user/lock/access details. **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ {} ] } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## Push Device Activity `POST` `https://api.igloohome.co/works/devices/{id}/activity` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` - **`logsPayload`** `array of string` *required* ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/activity" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "logsPayload": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/activity" payload := []byte(`{ "logsPayload": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/activity", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "logsPayload": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/activity" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "logsPayload": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/activity", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "logsPayload" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "logsPayload": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/activity") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/activity")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "logsPayload": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 '{msg: "Successfully."}', or '{msg: "no.activities.on.lock"}' if there is nothing new to process.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`msg`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "msg": "string" } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### FILE: works/api_departments Title: Departments Category: API Reference ---------------------------------------- # Departments API Reference API endpoints for managing departments. ## List Departments `GET` `https://api.igloohome.co/works/departments` ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/departments" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`departmentName`** `string` - **`propertyRef`** `array of object` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "departmentName": "string", "id": "string", "propertyRef": [ {} ] } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## List Department Properties `GET` `https://api.igloohome.co/works/departments/{departmentId}/properties` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | departmentId | string | Yes | ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/departments/{departmentId}/properties" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK. Empty 'payload' (not a 404) if the department has zero properties.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `409 Department not found.` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`totalLock`** `integer` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `409`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore", "totalLock": 12 } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `409`:** ```json { "error": "string" } ``` ## List Department Access `GET` `https://api.igloohome.co/works/departments/{id}/access` Same response shape as GET /devices/{id}/access, scoped to the department. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/departments/{id}/access" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Department not found.` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`name`** `string` - **`accessType`** `string` - Allowed values: `pin`, `ekey`, `nfc`, `fingerprint` - **`bluetoothDeviceName`** `string` - **`isCustomPin`** `boolean`\ Present only if accessType===pin. - **`pin`** `string`\ Present only if accessType===pin. - **`pinType`** `string`\ Present only if accessType===pin. The API stores and reports AlgoPIN daily/hourly variants as `duration`. - Allowed values: `otp`, `duration`, `permanent` - **`startDateTime`** `string` - **`endDateTime`** `string` - **`recipient`** `object`\ Present only if `accessType` is `ekey`. - **`name`** `string` - **`email`** `string` - **`createdAt`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "accessType": "pin", "bluetoothDeviceName": "IGH-LK-3F9A21", "createdAt": "2026-09-13T09:30:00+08:00", "endDateTime": "2026-09-14T18:00:00+08:00", "id": "556e1b3a8c2d4e5f6a7b8c8e", "isCustomPin": true, "name": "Temp PIN - Maintenance", "pin": "2847", "pinType": "duration", "startDateTime": "2026-09-13T10:00:00+08:00" } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## List Department Devices `GET` `https://api.igloohome.co/works/departments/{id}/devices` Same response shape as GET /devices, scoped to the department. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/departments/{id}/devices" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Department not found.` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`type`** `string` - Allowed values: `Lock`, `Bridge`, `Keypad`, `Fob` - **`deviceId`** `string` - **`deviceName`** `string` - **`batteryLevel`** `number` - **`pairedAt`** `string` - **`lockStatus`** `string`\ Present only on `GET /devices/{id}`, and only for a Lock linked to a Bridge. No list endpoint that returns this schema populates it. - **`properties`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`linkedDevices`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`linkedAccessories`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`lastSync`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "batteryLevel": 82.5, "deviceId": "IGH-LK-3F9A21", "deviceName": "Front Door Lock", "id": "665f1c2a9b3e4d5f6a7b8c9d", "lastSync": "2026-09-13T09:45:00Z", "linkedAccessories": [ { "deviceId": "IGH-BR-7C2E19", "id": "445d1a0b6c5e8d4f7a1b9c2e", "name": "Main Bridge", "type": "Bridge" } ], "linkedDevices": [], "lockStatus": "locked", "pairedAt": "2026-09-08T14:32:00Z", "properties": [ { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore" } ], "type": "Lock" } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### FILE: works/api_devices Title: Devices Category: API Reference ---------------------------------------- # Devices API Reference API endpoints for managing devices. ## List Devices `GET` `https://api.igloohome.co/works/devices` ### Query Parameters - **`type`** `string`\ Comma-separated, from: Lock, Bridge, Keypad, Fob. - **`search`** `string`\ Case-insensitive substring match against bluetoothDeviceName or name. - **`expand`** `string`\ Set to `propertyId` to populate full property objects on listingsRef. - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "type": "value", "search": "value", "expand": "value", "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices?type=value&search=value&expand=value&limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`type`** `string` - Allowed values: `Lock`, `Bridge`, `Keypad`, `Fob` - **`deviceId`** `string` - **`deviceName`** `string` - **`batteryLevel`** `number` - **`pairedAt`** `string` - **`lockStatus`** `string`\ Present only on `GET /devices/{id}`, and only for a Lock linked to a Bridge. No list endpoint that returns this schema populates it. - **`properties`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`linkedDevices`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`linkedAccessories`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`lastSync`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "batteryLevel": 82.5, "deviceId": "IGH-LK-3F9A21", "deviceName": "Front Door Lock", "id": "665f1c2a9b3e4d5f6a7b8c9d", "lastSync": "2026-09-13T09:45:00Z", "linkedAccessories": [ { "deviceId": "IGH-BR-7C2E19", "id": "445d1a0b6c5e8d4f7a1b9c2e", "name": "Main Bridge", "type": "Bridge" } ], "linkedDevices": [], "lockStatus": "locked", "pairedAt": "2026-09-08T14:32:00Z", "properties": [ { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore" } ], "type": "Lock" } ] } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## Get Device `GET` `https://api.igloohome.co/works/devices/{id}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Device not found.` **Response body — `200`:** - **`id`** `string` - **`type`** `string` - Allowed values: `Lock`, `Bridge`, `Keypad`, `Fob` - **`deviceId`** `string` - **`deviceName`** `string` - **`batteryLevel`** `number` - **`pairedAt`** `string` - **`lockStatus`** `string`\ Present only on `GET /devices/{id}`, and only for a Lock linked to a Bridge. No list endpoint that returns this schema populates it. - **`properties`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`linkedDevices`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`linkedAccessories`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`lastSync`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Example response — `200`:** ```json { "batteryLevel": 0, "deviceId": "string", "deviceName": "string", "id": "string", "lastSync": "string", "linkedAccessories": [ { "deviceId": "string", "id": "string", "name": "string", "type": "string" } ], "linkedDevices": [ { "deviceId": "string", "id": "string", "name": "string", "type": "string" } ], "lockStatus": "string", "pairedAt": "string", "properties": [ { "id": "string", "name": "string", "timezone": "string" } ], "type": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "message": "string" } ``` ## Get Admin Key `GET` `https://api.igloohome.co/works/devices/{id}/admin-key` Returns the device's obfuscated admin key. Supported for G2 devices only. G3 devices keep their admin key encrypted server-side, so this route cannot read it. The response is an obfuscated blob, keyed per device, that you must decrypt client-side. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}/admin-key" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/admin-key" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/admin-key", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/admin-key" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/admin-key", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/admin-key") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/admin-key")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `400 'unsupported.device.protocol' (not a G2 device), 'device.config.incomplete' (admin_key/password missing), or 'device.gmt_offset.not.configured'.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Device not found.` `500 '{message: "internal.server.error"}'. Uncaught exception.` **Response body — `200`:** - **`payload`** `string`\ Obfuscated blob. Decrypt client-side. **Response body — `400`:** - **`message`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` **Response body — `500`:** - **`message`** `string` **Example response — `200`:** ```json { "payload": "string" } ``` **Example response — `400`:** ```json { "message": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "message": "string" } ``` **Example response — `500`:** ```json { "message": "string" } ``` ### FILE: works/api_jobs Title: Jobs Category: API Reference ---------------------------------------- # Jobs API Reference API endpoints for managing jobs. ## Bridge Proxied Jobs ### Lock Jobs Bridge Proxied Jobs send commands to smart locks through a Bridge device. Your application calls the API; the Bridge talks to the lock over Bluetooth. Send these jobs with [Create Bridge Proxied Job](#create-bridge-proxied-job). #### Job Creation Create a job by making a POST request to: ``` /devices/{deviceId}/jobs/bridges/{bridgeId} ``` ##### Request Parameters | Parameter | Type | Description | | ----------- | -------- | ------------------------------------------------------ | | jobType | number | Command type identifier (see [Job Types](#job-types)) | | jobData | object | Command-specific parameters | | departmentId | string | Required unless the account has a single department | ##### Response ```json { "jobId": "string" } ``` ##### Job Types | Job Type | ID | | --- | --- | | Lock | 1 | | Unlock | 2 | | Create Custom PIN code | 4 | | Delete PIN code | 5 | | Get Battery Level | 9 | | Get Device Status | 10 | | Get Activity Logs | 15 | #### Command Details ##### Lock Locks the device and optionally synchronizes its internal clock. **Parameters** | Parameter | Type | Description | Required | | ----------- | ---------- | ------------------------------ | ---------- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | **Request Example** ```json { "jobType": 1, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` ##### Unlock Unlocks the device and optionally synchronizes its internal clock. **Parameters** | Parameter | Type | Description | Required | | ----------- | ---------- | ------------------------------ | ---------- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | **Request Example** ```json { "jobType": 2, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` ##### Create Custom PIN code Creates a PIN code with specific access permissions and time restrictions. **Parameters** | Parameter | Type | Description | Required | | ------------ | -------- | ------------------------------------------- | ---------- | | accessName | string | Display name for the PIN code, up to 60 characters | Yes | | pin | string | 4-6 digit PIN code | Yes | | pinType | number | PIN type (see table below) | Yes | | startDate | string | When PIN becomes active (ISO_DATE format) | Yes | | endDate | string | When PIN expires (ISO_DATE format) | Duration only | **PIN Types** | Type | ID | Description | | ----------- | ---- | ------------------------------------ | | One-Time | 1 | Valid for 24 hours from start date | | Permanent | 2 | Valid indefinitely from start date | | Duration | 4 | Valid between start and end dates | **Request Example** ```json { "jobType": 4, "jobData": { "accessName": "Guest PIN", "pin": "123456", "pinType": 4, "startDate": "2024-01-01T12:00:00+00:00", "endDate": "2024-01-01T16:00:00+00:00" } } ``` **Usage Examples** One-Time PIN (24-hour validity): ```json { "jobType": 4, "jobData": { "accessName": "OTP PIN", "pin": "123456", "pinType": 1, "startDate": "2024-01-01T00:00:00+00:00" } } ``` Permanent PIN: ```json { "jobType": 4, "jobData": { "accessName": "Permanent PIN", "pin": "123456", "pinType": 2, "startDate": "2024-01-01T00:00:00+00:00" } } ``` Duration PIN (specific time window): ```json { "jobType": 4, "jobData": { "accessName": "Duration PIN", "pin": "123456", "pinType": 4, "startDate": "2024-01-01T12:00:00+00:00", "endDate": "2024-01-01T16:00:00+00:00" } } ``` ##### Delete PIN code Removes a PIN access from the lock. Identify it by `accessId`, the ID of the PIN access record from [List Device Access](/works/api_access#list-device-access). > **Note**: An unactivated AlgoPIN with a start date more than 256 hours after its creation cannot be deleted. **Parameters** | Parameter | Type | Description | Required | | ----------- | -------- | ------------------------ | ---------- | | accessId | string | ID of the PIN access record to delete | Yes | **Request Example** ```json { "jobType": 5, "jobData": { "accessId": "665f1c2a9b3e4d5f6a7b8c9d" } } ``` ##### Get Battery Level Retrieves the lock's current battery level as a percentage. **Request Example** ```json { "jobType": 9 } ``` ##### Get Device Status Queries the lock's current operational status. **Request Example** ```json { "jobType": 10 } ``` ##### Get Activity Logs Downloads activity logs from the lock. Optionally synchronizes the lock's clock. > **Important**: Activity logs are deleted from the lock after retrieval. **Parameters** | Parameter | Type | Description | Required | | ----------- | ---------- | ------------------------------ | ---------- | | lockTime | ISO_DATE | Synchronize the lock's clock | No | **Request Example** ```json { "jobType": 15, "jobData": { "lockTime": "2024-01-01T12:00:00+00:00" } } ``` #### Error Status Codes | Status Code | Description | Applies To | | --- | --- | --- | | 400 | Request validation failed (missing or invalid field) | All | | 401 | Missing, invalid or expired access token | All | | 402 | Account suspended, or the iglooworks API trial has ended | All | | 403 | The token lacks the scope for this job type, or the user revoked access | All | | 404 | No PIN access matches `jobData.accessId` | Delete PIN code | | 406 | A pending job for the same PIN exists, or the PIN is already in use on this lock | Create Custom PIN code | | 409 | The lock is not associated with the given department | All | | 415 | Missing `Content-Type: application/json` | All | ### Get Bridge Job Status `GET` `https://api.igloohome.co/works/bridge/jobs/{jobId}` #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | jobId | string | Yes | #### Request Example ```bash curl -X GET "https://api.igloohome.co/works/bridge/jobs/{jobId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/bridge/jobs/{jobId}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/bridge/jobs/{jobId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/bridge/jobs/{jobId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/bridge/jobs/{jobId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/bridge/jobs/{jobId}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/bridge/jobs/{jobId}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Job not found.` `422 jobId is not a valid ID.` **Response body — `200`:** - **`jobId`** `string` - **`expiryDate`** `string` - **`completed`** `boolean` - **`jobType`** `string` - **`jobResponse`** `object` - **`jobStatus`** `integer`\ 0=complete, 1=pending, 2=expired. - **`opResult`** `string`\ Absent while the job is pending. Present once a result is recorded. **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "completed": true, "expiryDate": "2026-09-13T16:32:45.123Z", "jobId": "885f3d4a0d5f6e7a8b9c0d1e", "jobResponse": { "jobStatus": 0, "opResult": 0 }, "jobType": "CREATE_CUSTOM_PIN" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### Create Bridge Proxied Job `POST` `https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}` Sends a command to a lock through its Bridge: lock, unlock, create or delete a PIN, get status, or get activity logs. The shape of `jobData` depends on `jobType`. CREATE_CUSTOM_PIN and DELETE_PIN_CODE have their own sub-schemas; other job types accept an open object. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | | bridgeId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`jobType`** `integer` *required*\ 1=LOCK, 2=UNLOCK, 4=CREATE_CUSTOM_PIN, 5=DELETE_PIN_CODE, 9=GET_BATTERY_LEVEL, 10=GET_DEVICE_STATUS, 15=GET_ACTIVITY_LOGS. - Allowed values: `1`, `2`, `4`, `5`, `9`, `10`, `15` - **`jobData`** `object`\ Shape depends on jobType. See the operation summary. - **`departmentId`** `string`\ Required unless the account has a single department, in which case the API ignores it. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "departmentId": "value", "jobData": "value", "jobType": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}" payload := []byte(`{ "departmentId": "value", "jobData": "value", "jobType": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "departmentId": "value", "jobData": "value", "jobType": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "departmentId": "value", "jobData": "value", "jobType": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "departmentId" => "value", "jobData" => "value", "jobType" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "departmentId": "value", "jobData": "value", "jobType": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "departmentId": "value", "jobData": "value", "jobType": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 'jobType=5' (DELETE_PIN_CODE) only. No matching PIN access found for 'jobData.accessId'.` `406 'jobType=4' (CREATE_CUSTOM_PIN) only. A pending job for the same PIN already exists, or the PIN is already in use on this lock.` `409 The lock is not associated with the given department (or the account's only department, for a single-department account).` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`jobId`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`error`** `string` **Response body — `406`:** - **`error`** `string` **Response body — `409`:** - **`error`** `string` **Example response — `200`:** ```json { "jobId": "string" } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "error": "string" } ``` **Example response — `406`:** ```json { "error": "string" } ``` **Example response — `409`:** ```json { "error": "string" } ``` ## App Sync Jobs ### Delete PIN Access `DELETE` `https://api.igloohome.co/works/devices/{id}/access/{accessId}` Revokes a PIN access by creating an asynchronous delete job. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | | accessId | string | Yes | #### Request Example ```bash curl -X DELETE "https://api.igloohome.co/works/devices/{id}/access/{accessId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/access/{accessId}" req, _ := http.NewRequest("DELETE", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/access/{accessId}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/access/{accessId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.delete(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/access/{accessId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/access/{accessId}") .method("DELETE", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/access/{accessId}")!) request.httpMethod = "DELETE" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 Delete job created.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Access not found ('{error: "access not found"}').` `409 Response` `415 Missing 'Content-Type: application/json'. The API enforces this header even though this DELETE has no meaningful body fields.` **Response body — `200`:** - **`jobId`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`error`** `string` **Response body — `409`:** - **`error`** `string` **Example response — `200`:** ```json { "jobId": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "error": "string" } ``` **Example response — `409`:** ```json { "error": "string" } ``` ### List Device Jobs `GET` `https://api.igloohome.co/works/devices/{id}/jobs` #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Query Parameters - **`jobType`** `string`\ Comma-separated, from: delete_pin, create_bluetooth_pin, add_group_key, delete_group_key. - **`status`** `string` - Allowed values: `pending`, `complete`, `failed` - **`departmentId`** `string`\ Filter jobs to a single department (not comma-separated, unlike jobType). - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` #### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/jobs" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "jobType": "value", "status": "value", "departmentId": "value", "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/jobs?jobType=value&status=value&departmentId=value&limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`_id`** `string` - **`description`** `string` - **`status`** `string` - Allowed values: `pending`, `complete`, `failed` - **`accessData`** `object` - **`groupKeyData`** `object` - **`createdAt`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "_id": "665f1c2a9b3e4d5f6a7b8c9d", "accessData": { "accessType": "pin", "customPin": "5837", "endDateTime": "2026-09-20T18:00:00Z", "pinType": "duration", "startDateTime": "2026-09-15T10:00:00Z" }, "createdAt": "2026-09-13T14:32:45.123Z", "description": "create_bluetooth_pin", "status": "pending" } ] } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### Create Custom PIN `POST` `https://api.igloohome.co/works/devices/{id}/jobs` Creates an asynchronous job that sets a custom PIN on the lock over Bluetooth. #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | #### Body Parameters Content-Type: `application/json` - **`description`** `string` - **`departmentId`** `string`\ Required unless the account has a single department, in which case the API ignores it. - **`pinType`** `string` *required* - Allowed values: `duration`, `otp`, `permanent` - **`startDateTime`** `string` *required*\ Format `YYYY-MM-DDTHH:mm:ss+hh:mm`. At most 1 hour in the past. - **`endDateTime`** `string`\ Format `YYYY-MM-DDTHH:mm:ss+hh:mm`. Required only when pinType is duration (must be after startDateTime, at most 1 year later). Forbidden otherwise. - **`customPin`** `string` *required*\ 4-6 digits, sent as a string. #### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/jobs" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/jobs" payload := []byte(`{ "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/jobs", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/jobs" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/jobs", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "customPin" => "value", "departmentId" => "value", "description" => "value", "endDateTime" => "value", "pinType" => "value", "startDateTime" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/jobs") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/jobs")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "customPin": "value", "departmentId": "value", "description": "value", "endDateTime": "value", "pinType": "value", "startDateTime": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 Job created.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `409 Response` `415 Missing 'Content-Type: application/json'.` **Response body — `200`:** - **`payload`** `object` - **`_id`** `string` - **`description`** `string` - **`status`** `string` - Allowed values: `pending`, `complete`, `failed` - **`accessData`** `object` - **`accessType`** `string` - **`pinType`** `string` - **`startDateTime`** `string` - **`endDateTime`** `string` - **`description`** `string` - **`customPin`** `string` - **`createdAt`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `409`:** - **`msg`** `string` - **`code`** `integer` **Example response — `200`:** ```json { "createdAt": "string", "payload": { "_id": "string", "accessData": { "accessType": "string", "customPin": "string", "description": "string", "endDateTime": "string", "pinType": "string", "startDateTime": "string" }, "description": "string", "status": "string" } } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `409`:** ```json { "code": 0, "msg": "string" } ``` ### Get Job `GET` `https://api.igloohome.co/works/devices/{id}/jobs/{jobId}` #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | | jobId | string | Yes | #### Request Example ```bash curl -X GET "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/jobs/{jobId}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/jobs/{jobId}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Job not found.` **Response body — `200`:** - **`payload`** `object` - Example: `{"_id":"665f1c2a9b3e4d5f6a7b8c9d","accessData":{"accessType":"pin","customPin":"5837","endDateTime":"2026-09-20T18:00:00Z","pinType":"duration","startDateTime":"2026-09-15T10:00:00Z"},"createdAt":"2026-09-13T14:32:45.123Z","description":"create_bluetooth_pin","status":"pending"}` - **`_id`** `string` - **`description`** `string` - **`status`** `string` - Allowed values: `pending`, `complete`, `failed` - **`accessData`** `object` - **`groupKeyData`** `object` - **`createdAt`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`error`** `string` **Example response — `200`:** ```json { "payload": { "_id": "665f1c2a9b3e4d5f6a7b8c9d", "accessData": { "accessType": "pin", "customPin": "5837", "endDateTime": "2026-09-20T18:00:00Z", "pinType": "duration", "startDateTime": "2026-09-15T10:00:00Z" }, "createdAt": "2026-09-13T14:32:45.123Z", "description": "create_bluetooth_pin", "status": "pending" } } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "error": "string" } ``` ### Update Job Status `PATCH` `https://api.igloohome.co/works/devices/{id}/jobs/{jobId}` #### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | | jobId | string | Yes | #### Body Parameters Content-Type: `application/json` - **`status`** `string` *required* - Allowed values: `complete`, `failed` - **`reason`** `string`\ Stored only when status is failed. #### Request Example ```bash curl -X PATCH "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "reason": "value", "status": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" payload := []byte(`{ "reason": "value", "status": "value" }`) req, _ := http.NewRequest("PATCH", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/devices/{id}/jobs/{jobId}", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "reason": "value", "status": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "reason": "value", "status": "value" } response = requests.patch(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PATCH", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "reason" => "value", "status" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "reason": "value", "status": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/devices/{id}/jobs/{jobId}") .method("PATCH", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}")!) request.httpMethod = "PATCH" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "reason": "value", "status": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` #### Responses `200 OK. '{payload: null}' if the job no longer exists at update time.` `400 Query/body validation failure.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Job not found.` **Response body — `200`:** - **`payload`** `array` - **`_id`** `string` - **`status`** `string` - **`accessData`** `object` - **`groupKeyData`** `object` - **`updatedAt`** `string` **Response body — `400`:** - **`error`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`error`** `string` **Example response — `200`:** ```json { "payload": null } ``` **Example response — `400`:** ```json { "error": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "error": "string" } ``` ### FILE: works/api_properties Title: Properties Category: API Reference ---------------------------------------- # Properties API Reference API endpoints for managing properties. ## List Properties `GET` `https://api.igloohome.co/works/properties` ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/properties" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`totalLock`** `integer` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore", "totalLock": 12 } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## Get Property `GET` `https://api.igloohome.co/works/properties/{id}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/properties/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/properties/{id}" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/properties/{id}", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/properties/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/properties/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/properties/{id}") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/properties/{id}")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Property not found.` **Response body — `200`:** - **`payload`** `object` - Example: `{"id":"556e2b1a7c4d9e5f6a2b8c3d","name":"Riverside Apartments - Block A","timezone":"Asia/Singapore","totalLock":12}` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`totalLock`** `integer` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "payload": { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore", "totalLock": 12 } } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ## List Property Devices `GET` `https://api.igloohome.co/works/properties/{id}/devices` Same response shape as GET /devices, scoped to the property. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters - **`limit`** `integer` - Constraints: `>= 1`, `<= 300` - Default: `100` - **`cursor`** `string` - **`sort`** `string` - Default: `desc` - Allowed values: `asc`, `desc` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/properties/{id}/devices" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 Property not found.` **Response body — `200`:** - **`nextCursor`** `string`\ Present only if another page exists. - **`payload`** `array of object` - **`id`** `string` - **`type`** `string` - Allowed values: `Lock`, `Bridge`, `Keypad`, `Fob` - **`deviceId`** `string` - **`deviceName`** `string` - **`batteryLevel`** `number` - **`pairedAt`** `string` - **`lockStatus`** `string`\ Present only on `GET /devices/{id}`, and only for a Lock linked to a Bridge. No list endpoint that returns this schema populates it. - **`properties`** `array of object` - **`id`** `string` - **`name`** `string` - **`timezone`** `string` - **`linkedDevices`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`linkedAccessories`** `array of object` - **`id`** `string` - **`type`** `string` - **`deviceId`** `string` - **`name`** `string` - **`lastSync`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Example response — `200`:** ```json { "nextCursor": "string", "payload": [ { "batteryLevel": 82.5, "deviceId": "IGH-LK-3F9A21", "deviceName": "Front Door Lock", "id": "665f1c2a9b3e4d5f6a7b8c9d", "lastSync": "2026-09-13T09:45:00Z", "linkedAccessories": [ { "deviceId": "IGH-BR-7C2E19", "id": "445d1a0b6c5e8d4f7a1b9c2e", "name": "Main Bridge", "type": "Bridge" } ], "linkedDevices": [], "lockStatus": "locked", "pairedAt": "2026-09-08T14:32:00Z", "properties": [ { "id": "556e2b1a7c4d9e5f6a2b8c3d", "name": "Riverside Apartments - Block A", "timezone": "Asia/Singapore" } ], "type": "Lock" } ] } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` ### FILE: works/api_users Title: Users Category: API Reference ---------------------------------------- # Users API Reference API endpoints for managing users. ## List Users `GET` `https://api.igloohome.co/works/users` Returns a plain array of user records. Unlike other list endpoints, this response is not wrapped in `{payload: [...]}` and does not support pagination. ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/users" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/users" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/users", { method: "GET", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/users" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.get(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/users", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/users") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/users")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 OK. Raw array, not '{payload}'-wrapped.` `400 '{message: "Company ID missing", code: "COMPANY_ID_MISSING"}'.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `500 '{message: "Internal server error", code: "INTERNAL_SERVER_ERROR"}'.` **Response body — `400`:** - **`message`** `string` - **`code`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `500`:** - **`message`** `string` - **`code`** `string` **Example response — `400`:** ```json { "code": "string", "message": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `500`:** ```json { "code": "string", "message": "string" } ``` ## Create User `POST` `https://api.igloohome.co/works/users` Creates the user and sends them an invitation. ### Body Parameters Content-Type: `application/json` - **`email`** `string` *required* - **`firstName`** `string` *required* - **`lastName`** `string` *required* - **`userType`** `string` *required* - Allowed values: `ADMIN`, `MANAGER`, `GUEST` - **`departmentRef`** `array of string`\ Department IDs. Required for non-ADMIN users in multi-department accounts; not allowed in single-department accounts; ignored for ADMIN (admins get all departments). - **`mobileNumber`** `string`\ Requires mobileCountryCode in the same request. Must be unique within the account. - **`mobileCountryCode`** `string`\ Required if mobileNumber is set. Prefix such as `+65`, prepended to mobileNumber. - **`mfaPreference`** `boolean` ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/users" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/users" payload := []byte(`{ "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/users", { method: "POST", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/users" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/users", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "departmentRef" => "value", "email" => "value", "firstName" => "value", "lastName" => "value", "mfaPreference" => "value", "mobileCountryCode" => "value", "mobileNumber" => "value", "userType" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/users") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/users")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "departmentRef": "value", "email": "value", "firstName": "value", "lastName": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `201 Created.` `400 Bad Request` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `409 USER_ALREADY_EXISTS.` `500 FLAT_ORG_DEPT_NOT_FOUND, COGNITO_CREATE_FAILED, COGNITO_REF_MISSING, USER_RECORD_CREATION_FAILED.` **Response body — `201`:** - **`message`** `string` - **`user`** `object` **Response body — `400`:** - **`message`** `string` - **`code`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `409`:** - **`message`** `string` - **`code`** `string` **Response body — `500`:** - **`message`** `string` - **`code`** `string` **Example response — `201`:** ```json { "message": "string", "user": {} } ``` **Example response — `400`:** ```json { "code": "string", "message": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `409`:** ```json { "code": "string", "message": "string" } ``` **Example response — `500`:** ```json { "code": "string", "message": "string" } ``` ## Update User `PUT` `https://api.igloohome.co/works/users/{id}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` - **`fullName`** `string`\ Full name as one string. Create uses `firstName` and `lastName`; update takes `fullName` instead. - **`mobileCountryCode`** `string` - **`mobileNumber`** `string`\ Requires mobileCountryCode in the same request. Must be unique within the account. - **`lang`** `string` - **`userType`** `string` - Allowed values: `ADMIN`, `MANAGER`, `GUEST` - **`departmentRef`** `array of string`\ Department IDs. Replaces the user's current departments. Not allowed for single-department accounts or when userType is ADMIN. - **`mfaPreference`** `boolean` ### Request Example ```bash curl -X PUT "https://api.igloohome.co/works/users/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/users/{id}" payload := []byte(`{ "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }`) req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/users/{id}", { method: "PUT", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/users/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } payload = { "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" } response = requests.put(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/works/users/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PUT", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "departmentRef" => "value", "fullName" => "value", "lang" => "value", "mfaPreference" => "value", "mobileCountryCode" => "value", "mobileNumber" => "value", "userType" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/works/users/{id}") .method("PUT", body) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/users/{id}")!) request.httpMethod = "PUT" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "departmentRef": "value", "fullName": "value", "lang": "value", "mfaPreference": "value", "mobileCountryCode": "value", "mobileNumber": "value", "userType": "value" ] request.httpBody = try? JSONSerialization.data(withJSONObject: payload) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 Updated. Returns '{message: "No changes provided to update", user}' if every provided field matched the existing value.` `400 MOBILE_COUNTRY_CODE_REQUIRED, PHONE_NUMBER_IN_USE, DEPARTMENT_REF_NOT_ALLOWED, admin-cannot-have-department.` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `403 Forbidden. One of: (1) the token is valid but lacks the required scope for this endpoint (no body); (2) the user revoked the OAuth client's access ('{error: "The user has revoked access for this application..."}').` `404 USER_NOT_FOUND.` **Response body — `200`:** - **`message`** `string` - **`user`** `object` **Response body — `400`:** - **`message`** `string` - **`code`** `string` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `403`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`code`** `string` **Example response — `200`:** ```json { "message": "string", "user": {} } ``` **Example response — `400`:** ```json { "code": "string", "message": "string" } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `403`:** ```json { "error": "string" } ``` **Example response — `404`:** ```json { "code": "string", "message": "string" } ``` ## Delete User `DELETE` `https://api.igloohome.co/works/users/{id}` Behavior branches on whether the user has confirmed their invite yet: unconfirmed → cancels the invite and soft-deletes; confirmed → disables the user (`isEnabled: false`) and removes their eKey/MFA access instead of deleting the record outright. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Request Example ```bash curl -X DELETE "https://api.igloohome.co/works/users/{id}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/works/users/{id}" req, _ := http.NewRequest("DELETE", url, nil) req.Header.Set("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Set("Accept", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```javascript const res = await fetch("https://api.igloohome.co/works/users/{id}", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/works/users/{id}" headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json" } response = requests.delete(url, headers=headers) print(response.json()) ``` ```php "https://api.igloohome.co/works/users/{id}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_ACCESS_TOKEN", "Accept: application/json" ] ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val request = Request.Builder() .url("https://api.igloohome.co/works/users/{id}") .method("DELETE", null) .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN") .addHeader("Accept", "application/json") .build() val response = client.newCall(request).execute() println(response.body?.string()) ``` ```swift import Foundation var request = URLRequest(url: URL(string: "https://api.igloohome.co/works/users/{id}")!) request.httpMethod = "DELETE" request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { print(String(data: data, encoding: .utf8) ?? "") } } task.resume() ``` ### Responses `200 "User invitation cancelled successfully" or "User deactivated successfully".` `401 Unauthorized, invalid or expired authorization token` `402-account-suspended Payment Required. Account is billing-suspended or past its trial grace period.` `402-trial-ended Payment Required. iglooworks API trial has ended.` `404 USER_NOT_FOUND.` `409 USER_ACCEPTED_INVITATION (trying to cancel an invite that was already accepted).` **Response body — `200`:** - **`message`** `string` - **`user`** `object` **Response body — `402-account-suspended`:** - **`error`** `string` **Response body — `402-trial-ended`:** - **`error`** `string` **Response body — `404`:** - **`message`** `string` - **`code`** `string` **Response body — `409`:** - **`message`** `string` - **`code`** `string` **Example response — `200`:** ```json { "message": "string", "user": {} } ``` **Example response — `402-account-suspended`:** ```json { "error": "account.payment.is.suspended" } ``` **Example response — `402-trial-ended`:** ```json { "error": "30 days iglooworks API trial has ended" } ``` **Example response — `404`:** ```json { "code": "string", "message": "string" } ``` **Example response — `409`:** ```json { "code": "string", "message": "string" } ``` ## BLE SDK ### FILE: home/sdk_best_practices Title: Best Practices Category: BLE SDK ---------------------------------------- # Best Practices Patterns for building reliable apps with the igloohome BLE SDK. --- ## Architecture ``` ┌──────────────┐ │ Your App │ └──────┬───────┘ │ IglooPlugin API (suspend / Flow) ┌──────▼───────┐ │ Igloohome │──── REST API ────► Igloo Server │ SDK │ └──────┬───────┘ │ BLE ┌──────▼───────┐ │ Smart Lock │ └──────────────┘ ``` The SDK orchestrates BLE and server operations together. For example, `createPin()` writes the PIN to lock hardware via BLE, registers it on the server, and rolls back if the server call fails — all in one call. --- ## Singleton Pattern Only one `IglooPlugin` instance should exist at a time. Multiple instances hold separate connection state and will cause undefined BLE behavior. ```kotlin // Application-scoped singleton object SdkProvider { lateinit var sdk: IglooPlugin private set fun init(context: Context) { sdk = IglooPlugin(context.applicationContext) } } ``` ```swift // App-scoped singleton. IglooPlugin is @MainActor and holds no context/config — // init() takes no parameters. @MainActor enum SdkProvider { static let sdk = IglooPlugin() } ``` --- ## Token Management The igloohome SDK uses separate tokens for different operations: ### Sync Tokens The `sync()` method requires three dedicated tokens: | Token | Purpose | |-------|---------| | `getDeviceToken` | Reading device state from the server. | | `storeLogsToken` | Uploading activity logs to the server. | | `updateDeviceToken` | Patching device info (battery, firmware version). | ```kotlin val result = sdk.sync( deviceId = deviceId, key = guestKey, getDeviceToken = tokens.getDeviceToken, storeLogsToken = tokens.storeLogsToken, updateDeviceToken = tokens.updateDeviceToken, ) ``` ```swift let result = try await sdk.sync( deviceId, key: guestKey, getDeviceToken: tokens.getDeviceToken, storeLogsToken: tokens.storeLogsToken, updateDeviceToken: tokens.updateDeviceToken) ``` ### Access Token All other methods (`createPin`, `addKeycard`, `addFingerprint`, `link`, `checkFirmwareUpdate`, `performDfu`, etc.) accept a single `accessToken: String` parameter. This token is always required. ```kotlin sdk.createPin( deviceId = deviceId, key = guestKey, pin = "123456", pinType = PinType.PERMANENT, name = "Front Door PIN", accessToken = accessToken, ) ``` ```swift try await sdk.createPin( deviceId: deviceId, key: guestKey, name: "Front Door PIN", pin: "123456", pinType: .permanent, startDate: nil, endDate: nil, accessToken: accessToken) ``` --- ## Connection Management - **One operation at a time.** BLE operations are serialized internally per device — don't fire multiple lock/unlock calls concurrently on the same device. - **Disconnect when done.** Call `disconnect(deviceId)` after your operation completes to free BLE resources. - **The SDK connects automatically.** There is no separate `connect()` method — `lock()`, `sync()`, `pair()` etc. all connect internally. ```kotlin try { sdk.lock(deviceId, key) } finally { sdk.disconnect(deviceId) } ``` ```swift // Swift has no try/finally — use `defer` instead, which runs when the // enclosing scope exits whether `lock` throws or not. defer { Task { try? await sdk.disconnect(deviceId) } } try await sdk.lock(deviceId, key: key) ``` --- ## Coroutine Patterns All BLE operations are `suspend` functions or return `Flow`. Use structured concurrency. ### Suspend Functions ```kotlin // In a ViewModel viewModelScope.launch { try { sdk.lock(deviceId, key) _uiState.value = UiState.Success } catch (e: IglooHomeException) { _uiState.value = UiState.Error(e.message) } } ``` ### Flow Collection ```kotlin // Scanning — cancel when no longer needed private var scanJob: Job? = null fun startScan() { scanJob = viewModelScope.launch { sdk.scanDevice().collect { result -> _devices.value += result } } } fun stopScan() { scanJob?.cancel() } ``` ### Flow with Lifecycle ```kotlin // In a Fragment/Activity lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { sdk.addFingerprint(deviceId, key, name, accessToken).collect { event -> // Handle scan events } } } ``` --- ## Swift Concurrency Patterns All BLE operations are `async throws` functions or return `AsyncStream`/`AsyncThrowingStream`. Use structured concurrency (`Task`, `async let`, task cancellation) in place of Kotlin's coroutine scopes. ### Async Functions ```swift // In an @Observable / ObservableObject view model Task { do { try await sdk.lock(deviceId, key: key) uiState = .success } catch { uiState = .error(error.localizedDescription) } } ``` ### Stream Collection ```swift // Scanning — cancel when no longer needed private var scanTask: Task? func startScan() { scanTask = Task { do { for try await result in sdk.scansLock() { devices.append(result) } } catch { // Handle scan error } } } func stopScan() { sdk.stopScan() scanTask?.cancel() } ``` ### Stream with View Lifecycle ```swift // In a SwiftUI View, tied to the view's lifetime .task { for await event in await sdk.addFingerprint( deviceId: deviceId, key: key, name: name, accessToken: accessToken) { // Handle scan events } } ``` --- ## Error Handling Catch at the UI layer, not deep in the call stack. ```kotlin viewModelScope.launch { try { sdk.pair(deviceId, name, accessToken) _uiState.value = UiState.Paired } catch (e: IglooHomeException) { _uiState.value = when (e) { is IglooHomeException.DevicePairedException -> UiState.Error("Already paired") is IglooHomeException.HasLinkedDeviceException -> UiState.Error("Unlink ${e.linkedDeviceIds.size} accessories first") is IglooHomeException.ConnectionException -> UiState.Retryable("unexpected bluetooth connection issue") is IglooHomeException.TimeoutException -> UiState.Retryable("Connection lost — try again") else -> UiState.Error(e.message ?: "Unknown error") } } } ``` ```swift Task { do { try await sdk.pair(deviceId: deviceId, lockName: name, propertyIds: propertyIds, accessToken: accessToken) uiState = .paired } catch IgloohomeError.devicePaired { uiState = .error("Already paired") } catch IgloohomeError.hasLinkedDevice(let linkedDeviceIds) { uiState = .error("Unlink \(linkedDeviceIds.count) accessories first") } catch IgloohomeError.connection { uiState = .retryable("unexpected bluetooth connection issue") } catch IgloohomeError.timeout { uiState = .retryable("Connection lost — try again") } catch { uiState = .error(error.localizedDescription) } } ``` **Retry only transient errors:** `TimeoutException`, `ConnectionException`, and `ApiException` with 5xx status (Android); `IgloohomeError.timeout`, `.connection`, and `IgloohomeError.api` with 5xx status (iOS). Terminal errors like `DevicePairedException`/`IgloohomeError.devicePaired` or `LockStorageFullException`/`IgloohomeError.lockStorageFull` require user action. --- ## Recommended Operation Sequences ### First-Time Device Setup 1. `scanDevice()` (Android) / `scansLock()` (iOS) — find the lock 2. `pair()` — register on account 3. `calibrate()` — tune motor direction 4. `sync()` — set clock and read battery 5. `setWifiConfig()` — if bridge, configure WiFi ### Regular Operations 1. `lock()` / `unlock()` — control the lock 2. `sync()` — periodically sync time and logs ### Access Management 1. `createPin()` / `addKeycard()` / `addFingerprint()` — add access 2. `deletePin()` / `deleteKeycard()` / `deleteFingerprint()` — remove access ### Firmware Update 1. `checkFirmwareUpdate()` — check for updates 2. `performDfu()` — apply update (keep screen on) 3. `sync()` — verify device state after update ### Device Removal 1. `unlink()` — remove all accessories first 2. `unpair()` — remove the lock --- ## Troubleshooting | Problem | Cause | Solution | |---------|-------|----------| | `BluetoothException` on every call | Bluetooth disabled or permissions not granted | Check `BluetoothAdapter.isEnabled` and request runtime permissions | | `ConnectionException` frequently | Device out of BLE range | Move within 2–3 meters of the lock | | `TimeoutException` on first call | Lock in deep sleep | Retry — first connection wakes the lock | | `DevicePairedException` during pair | Lock already registered | Check server if you own it; factory reset if transferring | | `HasLinkedDeviceException` during unpair | Accessories still linked | Call `unlink()` for each accessory first | | `DuplicatePinException` | Same PIN exists on lock | Choose a different PIN code | | `LockStorageFullException` | Lock PIN/card slots exhausted | Delete existing access before adding new ones | | `BatteryLowException` during DFU | Battery below threshold | Charge or replace batteries before DFU | | Multiple `IglooPlugin` instances | Creating SDK in Activity/Fragment | Use application-scoped singleton | | BLE operations fail silently | RxJava undeliverable exceptions | SDK handles these internally — upgrade if on old version | The same table for iOS: | Problem | Cause | Solution | |---------|-------|----------| | `IgloohomeError.bluetoothIsTurnedOff` on every call | Bluetooth disabled, or `NSBluetoothAlwaysUsageDescription` missing from `Info.plist` | Enable Bluetooth; verify the Info.plist keys are present (iOS kills the app on first scan without them) | | `IgloohomeError.lockNotFound` / `.connection` frequently | Device out of BLE range | Move within 2–3 meters of the lock | | `IgloohomeError.timeout` on first call | Lock in deep sleep | Retry — first connection wakes the lock | | `IgloohomeError.devicePaired` during pair | Lock already registered | Check server if you own it; factory reset if transferring | | `IgloohomeError.hasLinkedDevice` during unpair | Accessories still linked | Call `unlink()` for each accessory first | | `IgloohomeError.duplicatePin` | Same PIN exists on lock | Choose a different PIN code | | `IgloohomeError.lockStorageFull` | Lock PIN/card slots exhausted | Delete existing access before adding new ones | | `IgloohomeError.batteryLow` during DFU | Battery below threshold | Charge or replace batteries before DFU | | Testing on Simulator does nothing | Bluetooth is unavailable in the iOS Simulator | Test on a physical device | | Two `CreateKeycardAccessResponse` / `CalibrationLockingDirection` types found | Both `IgloohomeSDK` and `IglooSDKCore` declare a type of that name (different raw values for `CalibrationLockingDirection`) | Import only `IgloohomeSDK` for app code where possible; be explicit about which module's type you mean if you also import `IglooSDKCore` | ### FILE: home/sdk_calibration Title: Calibration Category: BLE SDK ---------------------------------------- # Calibration Calibrate igloohome locks to tune motor direction and door settings. --- ## calibrate Calibrates a lock via Bluetooth. Lock type is resolved from the `deviceId` prefix and routes to the appropriate BLE flow. Returns a `Flow` — non-OE1 locks emit only `Complete`, OE1 emits interactive steps requiring user signals. After BLE calibration succeeds, the SDK updates the server with the calibration result. ### Signature ```kotlin fun calibrate( deviceId: String, key: String, lockingDirection: LockingDirection? = null, signals: ReceiveChannel? = null, accessToken: String, ): Flow ``` ```swift func calibrate( deviceId: String, key: String, lockingDirection: LockingDirection? = nil, signals: AsyncStream? = nil, accessToken token: String ) async throws -> AsyncThrowingStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `CALIBRATE` and `SET_BRIGHTNESS` permissions. | | `lockingDirection` | `LockingDirection?` | IGB4 only | Locking direction. Required for IGB4 locks, ignored for others. | | `signals` | `ReceiveChannel?` | OE1 only | Channel for interactive OE1 flow. Required for OE1, ignored for others. | | `accessToken` | `String` | Yes | OAuth access token for server calls. | ### Return Type — CalibrationStep ```kotlin sealed class CalibrationStep { /** OE1: User should open door ajar. Send CalibrationSignal.Proceed to continue. */ object OpenPosition : CalibrationStep() /** OE1: User should close door. Send CalibrationSignal.Proceed to continue. */ object ClosePosition : CalibrationStep() /** OE1: User should select locking direction and door type. Send DoorSettingsSelected. */ object DoorSettings : CalibrationStep() /** All BLE steps complete. */ data class Complete( val lockingDirection: LockingDirection, val cylinderRotation: CylinderRotation?, val unlockHoldEnabled: Boolean?, ) : CalibrationStep() } ``` ```swift public enum CalibrationStep : Equatable, Sendable { case openPosition case closePosition case doorSettings case complete(CalibrateResult) } public struct CalibrateResult : Sendable { public let deviceId: String public let lockingDirection: LockingDirection public let calibratedAt: String public let cylinderRotation: CylinderRotation? } ``` ### Supporting Types ```kotlin enum class LockingDirection { LEFT_HANDED, RIGHT_HANDED, UNKNOWN, } enum class CylinderRotation { SINGLE, DOUBLE, } sealed class CalibrationSignal { /** User is ready — proceed with current step. */ object Proceed : CalibrationSignal() /** User has selected door settings. */ data class DoorSettingsSelected( val lockingDirection: LockingDirection, val doorType: DoorType, ) : CalibrationSignal() } enum class DoorType { /** Both sides have a handle — unlockHoldEnabled = false */ BOTH_SIDES_HAVE_HANDLE, /** One side has no handle — unlockHoldEnabled = true */ ONE_SIDE_NO_HANDLE, } ``` ```swift enum LockingDirection : String, Sendable { case leftHanded = "left" case rightHanded = "right" } enum CylinderRotation : Sendable { case single case double } public enum CalibrationSignal : Sendable { case proceed case doorSettingsSelected(lockingDirection: LockingDirection, doorType: DoorType) } enum DoorType : Sendable { case bothSidesHaveHandle case oneSideNoHandle } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during calibration. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `InvalidLockingDirectionException` | 901 | Auto-detected locking direction is ambiguous. | | `CalibrationFailedException` | 902 | BLE calibration command failed. | | `ApiException` | varies | Server calibration update failed. | ### Example — Standard Lock (DAX, DBX) ```kotlin try { sdk.calibrate( deviceId = "DBX101abc123", key = guestKey, accessToken = accessToken, ).collect { step -> when (step) { is CalibrationStep.Complete -> { showSuccess("Calibration complete: ${step.lockingDirection}") } else -> { /* Only OE1 emits intermediate steps */ } } } } catch (e: IglooHomeException.CalibrationFailedException) { showError("Calibration failed — reposition the lock and try again") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException) { showError("Calibration failed: ${e.message}") } ``` ```swift do { let steps = try await sdk.calibrate( deviceId: "DBX101abc123", key: guestKey, accessToken: accessToken) for try await step in steps { switch step { case let .complete(result): showSuccess(message: "Calibration complete: \(result.lockingDirection.rawValue)") default: break // Only OE1 emits intermediate steps } } } catch IgloohomeError.calibrationFailed { showError(message: "Calibration failed — reposition the lock and try again") } catch IgloohomeError.connection { showError(message: "unexpected bluetooth connection issue") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Example — IGB4 Lock (requires lockingDirection) ```kotlin try { sdk.calibrate( deviceId = "IGB403fe3b9d", key = guestKey, lockingDirection = LockingDirection.LEFT_HANDED, accessToken = accessToken, ).collect { step -> when (step) { is CalibrationStep.Complete -> showSuccess("Calibrated: ${step.lockingDirection}") else -> {} } } } catch (e: IglooHomeException.InvalidLockingDirectionException) { showError("Locking direction is ambiguous — select manually") } catch (e: IglooHomeException.CalibrationFailedException) { showError("Calibration failed — try again") } catch (e: IglooHomeException) { showError("Calibration failed: ${e.message}") } ``` ```swift do { let steps = try await sdk.calibrate( deviceId: "IGB403fe3b9d", key: guestKey, lockingDirection: .leftHanded, accessToken: accessToken) for try await step in steps { switch step { case let .complete(result): showSuccess(message: "Calibrated: \(result.lockingDirection.rawValue)") default: break } } } catch IgloohomeError.invalidLockingDirection { showError(message: "Locking direction is ambiguous — select manually") } catch IgloohomeError.calibrationFailed { showError(message: "Calibration failed — try again") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Example — OE1 Lock (interactive flow) ```kotlin val signalChannel = Channel() // Collect in one coroutine launch { try { sdk.calibrate( deviceId = "OE1X01abc123", key = guestKey, signals = signalChannel, accessToken = accessToken, ).collect { step -> when (step) { is CalibrationStep.OpenPosition -> { showPrompt("Open the door ajar, then tap Continue") // User taps Continue button: signalChannel.send(CalibrationSignal.Proceed) } is CalibrationStep.ClosePosition -> { showPrompt("Close the door, then tap Continue") signalChannel.send(CalibrationSignal.Proceed) } is CalibrationStep.DoorSettings -> { showPrompt("Select locking direction and door type") signalChannel.send( CalibrationSignal.DoorSettingsSelected( lockingDirection = LockingDirection.LEFT_HANDED, doorType = DoorType.ONE_SIDE_NO_HANDLE, ) ) } is CalibrationStep.Complete -> { showSuccess("Calibration complete") signalChannel.close() } } } } catch (e: IglooHomeException.CalibrationFailedException) { showError("Calibration failed — reposition the door and try again") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException) { showError("Calibration failed: ${e.message}") } } ``` ```swift let (signalStream, signalContinuation) = AsyncStream.makeStream() do { let steps = try await sdk.calibrate( deviceId: "OE1X01abc123", key: guestKey, signals: signalStream, accessToken: accessToken) for try await step in steps { switch step { case .openPosition: showPrompt(message: "Open the door ajar, then proceed") signalContinuation.yield(.proceed) case .closePosition: showPrompt(message: "Close the door, then proceed") signalContinuation.yield(.proceed) case .doorSettings: showPrompt(message: "Select locking direction and door type, then confirm") signalContinuation.yield( .doorSettingsSelected(lockingDirection: .leftHanded, doorType: .oneSideNoHandle)) case let .complete(result): showSuccess(message: "Calibrated '\(result.deviceId)' — \(result.lockingDirection.rawValue)") signalContinuation.finish() } } } catch IgloohomeError.calibrationFailed { showError(message: "Calibration failed — reposition the door and try again") } catch IgloohomeError.connection { showError(message: "unexpected bluetooth connection issue") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Notes - The SDK disconnects from the lock automatically after calibration completes (success or failure). - Non-OE1 locks auto-detect locking direction during calibration (except IGB4 which requires it as a parameter). - OE1 locks require an interactive flow: the user positions the door and selects settings at each step. - After BLE calibration, the SDK sends the result to the server via PATCH `/devices/{deviceId}`. ### FILE: home/sdk_errors Title: Error Reference Category: BLE SDK ---------------------------------------- # Error Reference All exceptions thrown by the igloohome BLE SDK. --- ## Exception Hierarchy All SDK exceptions extend `IglooHomeException`, a sealed class with `status`, `source`, and `message` properties. ```kotlin sealed class IglooHomeException( val status: Int, val source: Throwable? = null, override val message: String? = null, ) : Exception(message, source) ``` On iOS, every method throws a single `Error` enum: `IgloohomeError`, so a single `catch IgloohomeError` (or a bare `catch { }`) is enough for every method. ```swift public enum IgloohomeError: Error { case bluetooth(source: Error?, message: String?) case bluetoothIsTurnedOff(source: Error? = nil, message: String? = nil) case lockNotFound(source: Error? = nil, message: String? = nil) case pairingFailed(source: Error? = nil, message: String? = nil) case connection(source: Error?, message: String?) case timeout(source: Error? = nil, message: String? = nil) case duplicatePin(source: Error? = nil, message: String? = nil) case pinNotFound(source: Error? = nil, message: String? = nil) case lockUidNotFound(source: Error? = nil, message: String? = nil) case bridgeOffline(source: Error? = nil, message: String? = nil) case invalidLockingDirection(source: Error? = nil, message: String? = nil) case calibrationFailed(source: Error? = nil, message: String? = nil) case lockStorageFull(source: Error? = nil, message: String? = nil) case maxScanAttempts(source: Error? = nil, message: String? = nil) case missingUid(source: Error? = nil, message: String? = nil) case batteryLow(source: Error? = nil, message: String? = nil) case dfuStateNotReady(source: Error? = nil, message: String? = nil) case dfuJobFailed(source: Error? = nil, message: String? = nil) case dfuJobExpired(source: Error? = nil, message: String? = nil) case devicePaired case hasLinkedDevice(linkedDeviceIds: [String]) case api(status: Int, source: Error? = nil, message: String? = nil) case dfuLibrary(status: Int, source: Error? = nil, message: String? = nil) case genericError(_ status: Int, source: Error? = nil, message: String? = nil) } // Conforms to LocalizedError — use `error.localizedDescription` for a formatted message. ``` --- ## Error Codes | Exception | Code | Description | |-----------|------|-------------| | `GenericException` | 1 | Catch-all for unhandled errors. | | `ConnectionException` | 12 | BLE connection failed or device disconnected unexpectedly. | | `BridgeOfflineException` | 406 | Bridge device is offline (cloud DFU / link operations). | | `TimeoutException` | 703 | Operation exceeded the time limit. Also used for `DfuJobFailedException` and `DfuJobExpiredException`. | | `DfuJobFailedException` | 703 | Cloud firmware update job failed. | | `DfuJobExpiredException` | 703 | Cloud firmware update job expired before completion. | | `BluetoothException` | 708 | Bluetooth is off or unavailable on the device. | | `DuplicatePinException` | 880 | PIN already exists on the lock. | | `PinNotFoundException` | 890 | PIN not found on the lock. | | `InvalidLockingDirectionException` | 901 | Auto-detected locking direction is ambiguous during calibration. | | `CalibrationFailedException` | 902 | BLE calibration command failed. | | `DevicePairedException` | 910 | Device is already paired. | | `HasLinkedDeviceException` | 910 | Device has linked accessories — unlink them first. Carries `linkedDeviceIds: List`. | | `LockStorageFullException` | 912 | Lock storage exhausted (no room for more PINs, keycards, or fingerprints). | | `MaxScanAttemptsException` | 972 | BLE scan retry limit exceeded. | | `LockUidNotFoundException` | 980 | Device UID not found on the lock (card/fingerprint already removed). | | `DfuStateNotReadyException` | 1003 | Device not ready for firmware update (door open, DFU mode not entered). | | `BatteryLowException` | 1010 | Battery too low for firmware update (< 20%, or < 31% for SP2X/SP2E). | | `DfuLibraryException` | 1100+ | Low-level DFU library error. Status varies. | | `ApiException` | varies | HTTP error from server API. Status matches the HTTP response code. | iOS does not use numeric status codes for most cases — only `IgloohomeError.api`, `.dfuLibrary`, and `.genericError` carry a `status: Int` (HTTP-style status from the Partner API or DFU library). The rest of the mapping is by case name: | Android exception | iOS equivalent | |---|---| | `GenericException` | `IgloohomeError.genericError` | | `ConnectionException` | `IgloohomeError.connection` | | `BridgeOfflineException` | `IgloohomeError.bridgeOffline` | | `TimeoutException` | `IgloohomeError.timeout` | | `DfuJobFailedException` | `IgloohomeError.dfuJobFailed` | | `DfuJobExpiredException` | `IgloohomeError.dfuJobExpired` | | `BluetoothException` | `IgloohomeError.bluetoothIsTurnedOff` | | `DuplicatePinException` | `IgloohomeError.duplicatePin` | | `PinNotFoundException` | `IgloohomeError.pinNotFound` | | `InvalidLockingDirectionException` | `IgloohomeError.invalidLockingDirection` | | `CalibrationFailedException` | `IgloohomeError.calibrationFailed` | | `DevicePairedException` | `IgloohomeError.devicePaired` | | `HasLinkedDeviceException` | `IgloohomeError.hasLinkedDevice(linkedDeviceIds:)` | | `LockStorageFullException` | `IgloohomeError.lockStorageFull` | | `MaxScanAttemptsException` | `IgloohomeError.maxScanAttempts` | | `LockUidNotFoundException` | `IgloohomeError.lockUidNotFound` | | `DfuStateNotReadyException` | `IgloohomeError.dfuStateNotReady` | | `BatteryLowException` | `IgloohomeError.batteryLow` | | `DfuLibraryException` | `IgloohomeError.dfuLibrary(status:)` | | `ApiException` | `IgloohomeError.api(status:)` | --- ## Error Handling Patterns ### Basic ```kotlin try { sdk.lock(deviceId, key) } catch (e: IglooHomeException) { showError("Error ${e.status}: ${e.message}") } ``` ```swift do { try await sdk.lock(deviceId, key: key) } catch { showError(message: error.localizedDescription) } ``` ### Exhaustive ```kotlin try { sdk.lock(deviceId, key) } catch (e: IglooHomeException) { when (e) { is IglooHomeException.BluetoothException -> showError("Please enable Bluetooth") is IglooHomeException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooHomeException.TimeoutException -> showError("Operation timed out, try again") else -> showError("Error: ${e.message}") } } ``` ```swift do { try await sdk.lock(deviceId, key: key) } catch IgloohomeError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch IgloohomeError.connection { showError(message: "unexpected bluetooth connection issue") } catch IgloohomeError.timeout { showError(message: "Operation timed out, try again") } catch { showError(message: "Error: \(error.localizedDescription)") } ``` > Every method — `lock`/`unlock` included — throws `IgloohomeError`, whether the failure came from the Bluetooth layer or the server, so one exhaustive `catch` list covers both. Add more `catch IgloohomeError.xxx { }` clauses for the cases a given method's own page calls out (`pair`, `createPin`, `link`, DFU, …). ### Flow-Based (DFU, Fingerprint) ```kotlin sdk.performDfu(deviceId, key, firmwareUid).collect { event -> when (event) { is DfuEvent.Progress -> updateProgressBar(event.percent) is DfuEvent.Complete -> showSuccess("Updated to ${event.newFirmwareVersion}") is DfuEvent.Failed -> showError("DFU failed: ${event.error.message}") } } ``` ```swift do { for try await event in sdk.performDfu(deviceId: deviceId, key: key, firmwareUid: firmwareUid, accessToken: accessToken) { switch event { case let .progress(percent): updateProgressBar(percent) case let .complete(newFirmwareVersion): showSuccess(message: "Updated to \(newFirmwareVersion)") } } } catch { showError(message: "DFU failed: \(error.localizedDescription)") } ``` --- ## Retryable vs Terminal | Type | Exceptions | Recommended Action | |------|-----------|-------------------| | Retryable | `TimeoutException`, `ConnectionException` | Retry the operation (user may need to move closer). | | Retryable | `ApiException` (5xx) | Retry after delay. | | Terminal | `BluetoothException` | Prompt user to enable Bluetooth. | | Terminal | `DevicePairedException` | Device already registered — no action needed. | | Terminal | `HasLinkedDeviceException` | Unlink accessories first using `unlink()`. | | Terminal | `DuplicatePinException` | Choose a different PIN. | | Terminal | `LockStorageFullException` | Delete existing access before adding new ones. | | Terminal | `BatteryLowException` | Wait for battery to charge before DFU. | | Terminal | `CalibrationFailedException` | Hardware issue — retry calibration from scratch. | | Non-fatal | `LockUidNotFoundException`, `PinNotFoundException` | SDK treats these as "already removed" during delete operations. | The same table on iOS, by case name: | Type | iOS cases | Recommended Action | |------|-----------|-------------------| | Retryable | `IgloohomeError.timeout`, `.connection`, `.lockNotFound` | Retry the operation (user may need to move closer, or wake the lock). | | Retryable | `IgloohomeError.api` (5xx) | Retry after delay. | | Retryable | `IgloohomeError.pairingFailed` | Retry pairing with the lock awake and close by. | | Terminal | `IgloohomeError.bluetoothIsTurnedOff` | Prompt user to enable Bluetooth. | | Terminal | `IgloohomeError.devicePaired` | Device already registered — no action needed. | | Terminal | `IgloohomeError.hasLinkedDevice` | Unlink accessories first using `unlink()`. | | Terminal | `IgloohomeError.duplicatePin` | Choose a different PIN. | | Terminal | `IgloohomeError.lockStorageFull` | Delete existing access before adding new ones. | | Terminal | `IgloohomeError.batteryLow` | Wait for battery to charge before DFU. | | Terminal | `IgloohomeError.calibrationFailed` | Hardware issue — retry calibration from scratch. | | Non-fatal | `IgloohomeError.lockUidNotFound`, `.pinNotFound` | SDK treats these as "already removed" during delete operations. | ### FILE: home/sdk_fingerprint Title: Fingerprint Category: BLE SDK ---------------------------------------- # Fingerprint Enroll and remove fingerprints on igloohome locks. --- ## addFingerprint Opens a BLE fingerprint registration window and returns a Flow of scan events. The lock requires 3 good reads to enroll a fingerprint (60-second timeout). On successful enrollment, the SDK registers the fingerprint on the server. If server registration fails, the SDK automatically removes the fingerprint from lock hardware. ### Signature ```kotlin fun addFingerprint( deviceId: String, key: String, name: String, accessToken: String, ): Flow ``` ```swift func addFingerprint( deviceId: String, key: String, name: String, accessToken token: String ) async -> AsyncStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `ADD_FINGERPRINT` permission. | | `name` | `String` | Yes | User-assigned name for the fingerprint. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Return Type — FingerprintScanEvent ```kotlin sealed class FingerprintScanEvent { /** Good read recorded. More reads needed. */ data class ScanProgress(val attempt: Int, val total: Int) : FingerprintScanEvent() /** Bad read. Prompt user to try again. */ class ScanError : FingerprintScanEvent() /** Enrollment completed and registered on server. */ data class Completed(val accessId: String, val name: String) : FingerprintScanEvent() /** Enrollment failed. */ data class Failed(val error: IglooHomeException) : FingerprintScanEvent() } ``` ```swift enum FingerprintScanEvent { /// A scan was captured, the user should place their finger on the sensor again. case scanProgress(attempt: Int, total: Int) /// The lock failed to read the fingerprint, the enrollment is stopped. case scanError /// Enrollment succeeded and the access is registered on the Iglooworks server. case completed(accessId: String, name: String) /// Enrollment failed with the given error. case failed(error: any Error) } ``` | Event | Description | |-------|-------------| | `ScanProgress(attempt, total)` | A good fingerprint scan. `attempt` = reads so far, `total` = 3. | | `ScanError` | Bad read — finger not positioned correctly. Prompt to retry. | | `Completed(accessId, name)` | All 3 reads successful. Fingerprint registered on server. | | `Failed(error)` | Enrollment failed (timeout, disconnect, etc.). | ### Example ```kotlin try { sdk.addFingerprint( deviceId = "IGM401abc123", key = guestKey, name = "Right Thumb", accessToken = accessToken, ).collect { event -> when (event) { is FingerprintScanEvent.ScanProgress -> showProgress("Scan ${event.attempt}/${event.total}") is FingerprintScanEvent.ScanError -> showWarning("Bad read — try again") is FingerprintScanEvent.Completed -> showSuccess("Fingerprint enrolled: ${event.accessId}") is FingerprintScanEvent.Failed -> { when (event.error) { is IglooHomeException.TimeoutException -> showError("Timed out — place finger on sensor and try again") is IglooHomeException.ConnectionException -> showError("unexpected bluetooth connection issue") else -> showError("Failed: ${event.error.message}") } } } } } catch (e: IglooHomeException.BluetoothException) { showError("Please enable Bluetooth") } catch (e: IglooHomeException) { showError("Fingerprint enrollment failed: ${e.message}") } ``` ```swift let stream = await sdk.addFingerprint( deviceId: "IGM401abc123", key: guestKey, name: "Right Thumb", accessToken: accessToken) var lastAttemp = 0, lastTotal = 0 for await event in stream { switch event { case let .scanProgress(attempt, total): showProgress( "Add Fingerprint", message: "Scanning \(attempt)/\(total) — place the finger on the reader", status: .warning) case .scanError: showProgress("Add Fingerprint", message: "Scan error, please try again", status: .failure) case let .completed(accessId, name): showProgress( "Add Fingerprint", message: "Fingerprint '\(name)' added. Access ID: \(accessId)", status: .success) case let .failed(error): showProgress("Add Fingerprint", message: error.localizedDescription, status: .failure) @unknown default: showProgress("Add Fingerprint", message: "Unknown event", status: .failure) } } ``` ### Notes - The lock requires exactly 3 good reads to enroll a fingerprint. - Bad reads don't count toward the total — the user just tries again. - The entire flow times out after 60 seconds. - Errors are emitted as `Failed` events rather than thrown as exceptions. --- ## deleteFingerprint Removes a fingerprint from the lock and deletes the server record. The SDK fetches the fingerprint UID from the server, removes it from lock hardware via BLE, then deletes the server record. ### Signature ```kotlin suspend fun deleteFingerprint( deviceId: String, key: String, accessId: String, accessToken: String, ) ``` ```swift func deleteFingerprint( deviceId: String, key: String, accessId: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_FINGERPRINT_UID` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the fingerprint to delete. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deleteFingerprint( deviceId = "IGM401abc123", key = guestKey, accessId = "access-789", accessToken = accessToken, ) showSuccess("Fingerprint removed") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Failed: ${e.message}") } ``` ```swift do { try await sdk.deleteFingerprint( deviceId: "IGM401abc123", key: guestKey, accessId: "access-789", accessToken: accessToken) showSuccess(message: "Fingerprint removed") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Server deletion retries up to 3 times with 1-second delays. - `LockUidNotFoundException` (980) during BLE delete is non-fatal — server record is still removed. ### FILE: home/sdk_firmware Title: Firmware Updates Category: BLE SDK ---------------------------------------- # Firmware Updates Check for and perform firmware updates on igloohome devices. --- ## checkFirmwareUpdate Checks whether a firmware update is available for the device. For BLE locks: reads the current firmware version from the lock over BLE, patches the server, then queries the DFU service. For EB1 bridges: reads the server-stored firmware version (no BLE) and queries the DFU service. ### Signature ```kotlin suspend fun checkFirmwareUpdate( deviceId: String, key: String, accessToken: String, ): FirmwareUpdateInfo ``` ```swift func checkFirmwareUpdate( deviceId: String, key: String, accessToken token: String ) async throws -> FirmwareUpdateInfo ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name (or device ID for EB1). | | `key` | `String` | Yes | eKey with `GET_FIRMWARE_VERSION` permission. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Return Type ```kotlin data class FirmwareUpdateInfo( val hasUpdate: Boolean, val currentVersion: String, val firmwareUid: String?, val latestFirmwareUid: String?, val firmwareSizeBytes: Long?, ) ``` ```swift public struct FirmwareUpdateInfo { public let hasUpdate: Bool public let currentVersion: String public let firmwareUid: String? public let firmwareSizeBytes: Int? } ``` | Field | Description | |-------|-------------| | `hasUpdate` | `true` if a newer firmware is available. | | `currentVersion` | Current firmware version string on the device. | | `firmwareUid` | UID of the available firmware package. `null` if no update. | | `latestFirmwareUid` | UID of the latest available firmware. `null` if no update. | | `firmwareSizeBytes` | Size of the firmware binary in bytes. `null` if no update. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable (BLE path). | | `ConnectionException` | 12 | Device disconnected during version read. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { val info = sdk.checkFirmwareUpdate( deviceId = "IGM401abc123", key = guestKey, accessToken = accessToken, ) if (info.hasUpdate) { println("Update available: ${info.firmwareUid}") println("Size: ${info.firmwareSizeBytes} bytes") } else { println("Firmware is up to date (${info.currentVersion})") } } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out reading firmware version") } catch (e: IglooHomeException) { showError("Check failed: ${e.message}") } ``` ```swift do { let info = try await sdk.checkFirmwareUpdate( deviceId: "IGM401abc123", key: guestKey, accessToken: accessToken) if info.hasUpdate { print("Update available: \(info.firmwareUid ?? "")") print("Size: \(info.firmwareSizeBytes ?? 0) bytes") } else { print("Firmware is up to date (\(info.currentVersion))") } } catch { showError(message: "Check failed: \(error.localizedDescription)") } ``` --- ## performDfu Performs a firmware update on the device. Returns a Flow of `DfuEvent` — progress updates then complete or failed. For BLE locks: downloads firmware, executes BLE DFU transfer with progress (0–100%), then patches the server with the new version. For SP2 devices: perform a Lock followed by an Unlock on the lock, then ensure the shackle is fully released (open position) before calling `performDfu`. For EB1 bridges: creates a cloud firmware update job and polls until completion. ### Signature ```kotlin fun performDfu( deviceId: String, key: String, firmwareUid: String, accessToken: String, ): Flow ``` ```swift func performDfu( deviceId: String, key: String, firmwareUid: String, accessToken token: String ) -> AsyncThrowingStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name (or device ID for EB1). | | `key` | `String` | Yes | eKey with `ENABLE_DFU` permission. | | `firmwareUid` | `String` | Yes | Firmware package UID from `FirmwareUpdateInfo.firmwareUid`. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Return Type — DfuEvent ```kotlin sealed class DfuEvent { /** Firmware transfer progress (0–100). */ data class Progress(val percent: Int) : DfuEvent() /** Firmware update completed. */ data class Complete(val newFirmwareVersion: String) : DfuEvent() /** Firmware update failed. */ data class Failed(val error: IglooHomeException) : DfuEvent() } ``` ```swift public enum DfuEvent { /// Firmware transfer progress, in percent (0–100). case progress(Int) /// Firmware update finished and the new version has been reported to the server. case complete(newFirmwareVersion: String) } ``` ### Example ```kotlin val info = sdk.checkFirmwareUpdate(deviceId, key, accessToken) if (!info.hasUpdate) return try { sdk.performDfu( deviceId = deviceId, key = key, firmwareUid = info.firmwareUid!!, accessToken = accessToken, ).collect { event -> when (event) { is DfuEvent.Progress -> updateProgressBar(event.percent) is DfuEvent.Complete -> showSuccess("Updated to ${event.newFirmwareVersion}") is DfuEvent.Failed -> { when (event.error) { is IglooHomeException.BatteryLowException -> showError("Battery too low for update") is IglooHomeException.DfuStateNotReadyException -> showError("Lock not ready — close the door and try again") else -> showError("DFU failed: ${event.error.message}") } } } } } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException) { showError("Firmware update failed: ${e.message}") } ``` ```swift let info = try await sdk.checkFirmwareUpdate(deviceId: deviceId, key: key, accessToken: accessToken) guard info.hasUpdate, let firmwareUid = info.firmwareUid else { return } do { for try await event in sdk.performDfu( deviceId: deviceId, key: key, firmwareUid: firmwareUid, accessToken: accessToken) { switch event { case let .progress(percent): updateProgressBar(percent) case let .complete(newFirmwareVersion): showSuccess(message: "Updated to \(newFirmwareVersion)") } } } catch IgloohomeError.batteryLow { showError(message: "Battery too low for update") } catch IgloohomeError.dfuStateNotReady { showError(message: "Lock not ready — close the door and try again") } catch { showError(message: "Firmware update failed: \(error.localizedDescription)") } ``` ### Notes - **Battery check:** The SDK checks battery before starting DFU. Minimum 20% (31% for SP2X/SP2E). Throws `BatteryLowException` if too low. - **DFU state check:** The SDK verifies the lock is ready for DFU (door closed, not in active state). Throws `DfuStateNotReadyException` if not ready. Skipped for EK and IEF SKU. - **SP2 SKU:** Before performing DFU on an SP2 device, perform a Lock followed by an Unlock on the lock, then ensure the shackle is fully released (open position) before starting the update. - **Progress:** BLE DFU emits progress 0–90% during transfer, animates 90–99% during reconnect, then 100% after reading the new firmware version. - **Bridge DFU:** For EB1 bridges, the SDK creates a cloud job and polls every 3 seconds. `DfuJobFailedException` if the job fails, `DfuJobExpiredException` if it expires. - **Keep screen alive:** DFU can take several minutes. Keep the screen on and display a progress indicator. - Errors are emitted as `DfuEvent.Failed` rather than thrown as exceptions. ### FILE: home/sdk_getting_started Title: SDK Setup Category: BLE SDK ---------------------------------------- # SDK Setup Install the igloohome BLE SDK and make your first lock operation. --- ## Requirements | Requirement | Version | |-------------|---------| | Android minSdk | 24 (Android 7.0) | | Android compileSdk | 35 | | Kotlin | 2.0.21+ | | Java compatibility | 1.8 | | iOS deployment target | 15.0+ | | Xcode | 16+ | | Swift | 6.0+ (Swift 6 language mode) | > BLE requires a physical iOS device — the iOS Simulator does not support Bluetooth. --- ## Installation (Android) ### 1. Add the GitLab Maven Repository In your project-level `settings.gradle.kts`: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://gitlab.com/api/v4/projects/64441730/packages/maven") credentials(HttpHeaderCredentials::class) { name = "Private-Token" value = providers.gradleProperty("iglooGitlabToken").get() } authentication { create("header") } } } } ``` Add your GitLab Personal Access Token to `gradle.properties`: ```properties iglooGitlabToken=glpat-XXXXXXXXXXXXXXXXXXXX ``` > **Security:** Do not commit `gradle.properties` with tokens to version control. Add it to `.gitignore`. ### 2. Add the SDK Dependency In your module-level `build.gradle.kts`: ```kotlin dependencies { implementation("co.igloo.home:sdk:3.5.0-build.2") } ``` --- ## Installation (iOS) ### 1. Add the Package Via Swift Package Manager, in Xcode: **File → Add Package Dependencies…**, or in `Package.swift`: ```swift dependencies: [ .package(url: "https://gitlab.com/igloohome/iglooaccess-ios-sdk.git", from: "3.5.0") ] ``` Add the `IgloohomeSDK` product to your target's dependencies. > **Note:** the package repo is private — Xcode must be configured with credentials (SSH key or a GitLab personal access token) that can read `gitlab.com/igloohome/iglooaccess-ios-sdk`. Alternatively, via CocoaPods: ```ruby pod 'IgloohomeSDK', :git => 'https://gitlab.com/igloohome/iglooaccess-ios-sdk.git', :tag => '3.5.0' ``` Set **Build Libraries for Distribution** (`BUILD_LIBRARY_FOR_DISTRIBUTION = YES`) on your app target — the SDK links a prebuilt `IglooSDKCore.xcframework`. ### 2. Import ```swift import IgloohomeSDK import IglooSDKCore ``` `IglooSDKCore` provides the shared enums/structs used across the API (`ScanResult`, `LockingDirection`, `CalibrationStep`, etc.) and must be imported alongside `IgloohomeSDK` even though the entry point (`IglooPlugin`) lives in `IgloohomeSDK`. --- ## Permissions ### AndroidManifest.xml ```xml ``` ### Info.plist (iOS) ```xml NSBluetoothAlwaysUsageDescription This application uses Bluetooth to connect to the Lock NSBluetoothPeripheralUsageDescription This application uses Bluetooth peripherals ``` ### Runtime Permissions Request BLE permissions before calling any SDK method: ```kotlin private val blePermissions = arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.ACCESS_FINE_LOCATION, ) private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { grants -> if (grants.values.all { it }) { // All permissions granted — safe to use SDK } } // Call before first SDK use permissionLauncher.launch(blePermissions) ``` There is no iOS equivalent of this step — just declare the `Info.plist` keys above and call SDK methods directly; the OS handles prompting. --- ## Initialization ### OAuth Authentication ```kotlin val sdk = IglooPlugin(context) // Pass accessToken to each method that requires it ``` ```swift let sdk = IglooPlugin() // Pass accessToken to each method that requires it ``` > **Important:** Only one `IglooPlugin` instance should exist at a time. Multiple instances cause undefined BLE behavior — each holds its own connection state. --- ## Quick Start ```kotlin // 1. Initialize val sdk = IglooPlugin(context) // 2. Scan for nearby locks try { sdk.scanDevice().collect { result -> println("Found: ${result.deviceId}, paired=${result.isPaired}, rssi=${result.rssi}") } } catch (e: IglooHomeException.BluetoothException) { println("Please enable Bluetooth") } catch (e: IglooHomeException) { println("Scan failed: ${e.message}") } // 3. Lock a device try { sdk.lock(deviceId = "IGM401abc123", key = guestKey) println("Locked") } catch (e: IglooHomeException.TimeoutException) { println("Lock not in range") } catch (e: IglooHomeException) { println("Error: ${e.message}") } // 4. Sync device state try { val sync = sdk.sync( deviceId = "IGM401abc123", key = guestKey, getDeviceToken = accessToken, storeLogsToken = accessToken, updateDeviceToken = accessToken, ) println("Battery: ${sync.batteryLevel}%") } catch (e: IglooHomeException.ConnectionException) { println("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { println("Timed out, try again") } catch (e: IglooHomeException) { println("Sync failed: ${e.message}") } // 5. Disconnect when done sdk.disconnect("IGM401abc123") ``` ```swift // 1. Initialize let sdk = IglooPlugin() // 2. Scan for nearby locks let scanTask = Task { do { for try await result in sdk.scansLock() { print("Found: \(result.bluetoothDeviceName), paired=\(result.isPaired)") } } catch IgloohomeError.bluetoothIsTurnedOff { print("Please enable Bluetooth") } catch { print("Scan failed: \(error.localizedDescription)") } } // Stop scanning once you've found what you need: sdk.stopScan() scanTask.cancel() // 3. Lock a device do { try await sdk.lock("IGM401abc123", key: guestKey) print("Locked") } catch IgloohomeError.timeout { print("Lock not in range") } catch { print("Error: \(error.localizedDescription)") } // 4. Sync device state do { let result = try await sdk.sync( "IGM401abc123", key: guestKey, getDeviceToken: accessToken, storeLogsToken: accessToken, updateDeviceToken: accessToken) print("Battery: \(result.batteryLevel)%") } catch { print("Sync failed: \(error.localizedDescription)") } // 5. Disconnect when done try? await sdk.disconnect("IGM401abc123") ``` --- ## Next Steps - [Lock & Unlock](/home/sdk_lock_unlock) — Control locks via BLE - [Scanning](/home/sdk_scanning) — Discover nearby devices - [Pairing](/home/sdk_pairing) — Register new locks - [Error Reference](/home/sdk_errors) — All exception types and codes ### FILE: home/sdk_keycard Title: Keycard (RFID) Category: BLE SDK ---------------------------------------- # Keycard (RFID) Add and remove RFID keycards on igloohome locks. --- ## addKeycard Opens a BLE registration window and waits for the user to tap a physical keycard on the lock's reader (45-second timeout). Once detected, registers the card on the server. If server registration fails after the card is enrolled on lock hardware, the SDK automatically removes the card from the lock. ### Signature ```kotlin suspend fun addKeycard( deviceId: String, key: String, name: String, accessToken: String, ): AddKeycardResponse ``` ```swift func addKeycard( deviceId: String, key: String, name: String, accessToken token: String ) async throws -> CreateKeycardAccessResponse ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `ADD_CARD` permission. | | `name` | `String` | Yes | User-assigned name for the keycard. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Return Type ```kotlin data class AddKeycardResponse( val accessId: String, val name: String, val payload: String, ) ``` ```swift public struct CreateKeycardAccessResponse: Codable, Sendable { public let accessId: String public let name: String public let payload: String } ``` | Field | Description | |-------|-------------| | `accessId` | Server-assigned access ID. | | `name` | The name assigned to the keycard. | | `payload` | Base64-encoded card UID. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during registration. | | `TimeoutException` | 703 | No card tapped within 45 seconds. | | `LockStorageFullException` | 912 | No storage for more keycards. | | `ApiException` | varies | Server registration failed (card rolled back from lock). | ### Example ```kotlin try { showPrompt("Tap your keycard on the lock reader...") val result = sdk.addKeycard( deviceId = "IGM401abc123", key = guestKey, name = "Office Badge", accessToken = accessToken, ) showSuccess("Keycard registered: ${result.accessId}") } catch (e: IglooHomeException.TimeoutException) { showError("No card detected — try again") } catch (e: IglooHomeException.LockStorageFullException) { showError("Lock is full — delete a keycard first") } catch (e: IglooHomeException) { showError("Failed: ${e.message}") } ``` ```swift do { showPrompt(message: "Tap your keycard on the lock reader...") let result = try await sdk.addKeycard( deviceId: "IGM401abc123", key: guestKey, name: "Office Badge", accessToken: accessToken) showSuccess(message: "Keycard registered: \(result.accessId)") } catch IgloohomeError.timeout { showError(message: "No card detected — try again") } catch IgloohomeError.lockStorageFull { showError(message: "Lock is full — delete a keycard first") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## deleteKeycard Removes a keycard from the lock and deletes the server record. The SDK fetches the card UID from the server, removes it from lock hardware via BLE, then deletes the server record. If the card is already removed from the lock (BLE error 980), the SDK still deletes the server record. ### Signature ```kotlin suspend fun deleteKeycard( deviceId: String, key: String, accessId: String, accessToken: String, ) ``` ```swift func deleteKeycard( deviceId: String, key: String, accessId: String, accessType: AccessType = .rfid, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_CARD` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the keycard to delete. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deleteKeycard( deviceId = "IGM401abc123", key = guestKey, accessId = "access-456", accessToken = accessToken, ) showSuccess("Keycard removed") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Failed: ${e.message}") } ``` ```swift do { try await sdk.deleteKeycard( deviceId: "IGM401abc123", key: guestKey, accessId: "access-456", accessToken: accessToken) showSuccess(message: "Keycard removed") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Registration timeout is 45 seconds (hardcoded). - Server deletion retries up to 3 times with 1-second delays. - `LockUidNotFoundException` (980, Android) / `IgloohomeError.lockUidNotFound` (iOS) during BLE delete is non-fatal — server record is still removed. - Supported lock types: IWS, IGM3, IGM4, IGR, RG1, MP1F, ML5, RW1. ### FILE: home/sdk_linking Title: Linking Category: BLE SDK ---------------------------------------- # Linking Link and unlink accessories (Keypads, Key Fobs, Bridges) to igloohome locks. --- ## link Links an accessory to a lock. The SDK orchestrates server registration, cipher resolution, and BLE link command. If the BLE step fails, the server link is automatically rolled back. ### Signature ```kotlin suspend fun link( accessoryDeviceId: String, accessoryGuestKey: String, lockDeviceId: String, accessToken: String, ): LinkResult ``` ```swift func link( accessoryDeviceId: String, accessoryGuestKey: String, lockBluetoothDeviceName: String, accessToken token: String ) async throws -> LinkResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `accessoryDeviceId` | `String` | Yes | Bluetooth device name of the accessory (e.g. `"EK1X01abc123"`). | | `accessoryGuestKey` | `String` | Yes | BLE key for the accessory. Keypad: eKey with `ADD_LOCK` permission. Fob: admin key. | | `lockDeviceId` | `String` | Yes | Bluetooth device ID of the lock to link to (e.g. `"IGM401abc123"`). | | `accessToken` | `String` | Yes | Access token for server calls. | ### Return Type ```kotlin data class LinkResult( val accessoryId: String, val lockId: String, val linkedAt: String, ) ``` ```swift public struct LinkResult { public let accessoryId: String public let lockId: String public let linkedAt: String } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during linking. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `BridgeOfflineException` | 406 | Bridge is offline (bridge link path). | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { val result = sdk.link( accessoryDeviceId = "EK1X01abc123", accessoryGuestKey = ekeyWithAddLock, lockDeviceId = "IGM401abc123", accessToken = accessToken, ) showSuccess("Linked ${result.accessoryId} to ${result.lockId}") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Link failed: ${e.message}") } ``` ```swift do { let result = try await sdk.link( accessoryDeviceId: "EK1X01abc123", accessoryGuestKey: ekeyWithAddLock, lockBluetoothDeviceName: "IGM401abc123", accessToken: accessToken) showSuccess(message: "Linked \(result.accessoryId) to \(result.lockId)") } catch { showError(message: "Link failed: \(error.localizedDescription)") } ``` --- ## unlink Unlinks an accessory from a lock. The SDK removes the link via BLE, then deletes the server record. BLE errors 890 (`PinNotFoundException`) and 980 (`LockUidNotFoundException`) are treated as "already unlinked on the accessory side" — the SDK continues to remove the server link. ### Signature ```kotlin suspend fun unlink( accessoryDeviceId: String, accessoryGuestKey: String, lockDeviceId: String, accessToken: String, ) ``` ```swift func unlink( accessoryDeviceId: String, accessoryGuestKey: String, lockBluetoothDeviceName: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `accessoryDeviceId` | `String` | Yes | Bluetooth device name of the accessory. | | `accessoryGuestKey` | `String` | Yes | BLE key for the accessory. Keypad: eKey with `DELETE_LOCK` permission. Fob: admin key. | | `lockDeviceId` | `String` | Yes | Bluetooth device ID of the lock to unlink from (e.g. `"IGM401abc123"`). | | `accessToken` | `String` | Yes | Access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during unlinking. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `BridgeOfflineException` | 406 | Bridge is offline (bridge unlink path). | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { sdk.unlink( accessoryDeviceId = "EK1X01abc123", accessoryGuestKey = ekeyWithDeleteLock, lockDeviceId = "IGM401abc123", accessToken = accessToken, ) showSuccess("Accessory unlinked") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Unlink failed: ${e.message}") } ``` ```swift do { try await sdk.unlink( accessoryDeviceId: "EK1X01abc123", accessoryGuestKey: ekeyWithDeleteLock, lockBluetoothDeviceName: "IGM401abc123", accessToken: accessToken) showSuccess(message: "Accessory unlinked") } catch IgloohomeError.bridgeOffline { showError(message: "Bridge is offline — try again once it's back online") } catch { showError(message: "Unlink failed: \(error.localizedDescription)") } ``` ### Notes - Always unlink accessories before unpairing the lock — `unpair()` will throw `HasLinkedDeviceException` (Android) / `IgloohomeError.hasLinkedDevice` (iOS) if linked devices exist. - The accessory key type depends on the accessory type: - **Keypad (EK1, EK2):** Generate an eKey with appropriate permission via the eKey API. - **Key Fob (IEF):** Use the admin key from `GET /devices/{device_id}/admin-key`. - Bridge accessories (`"EB1"` prefix) are dispatched to a server-job link/unlink flow instead of a BLE flow — `link`/`unlink` work for Bridges, throwing `BridgeOfflineException` (Android) / `IgloohomeError.bridgeOffline` (iOS) if the Bridge is offline. ### FILE: home/sdk_lock_unlock Title: Lock & Unlock Category: BLE SDK ---------------------------------------- # Lock & Unlock Control igloohome smart locks via BLE. --- ## lock Locks the device via Bluetooth. Optionally sets the lock's internal clock before locking. ### Signature ```kotlin suspend fun lock( deviceId: String, key: String, timeInSeconds: Long? = null, operationId: Int? = null, ) ``` ```swift func lock( _ deviceId: String, key: String, operationId: Int? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock (e.g. `"IGM401abc123"`). | | `key` | `String` | Yes | eKey for BLE authentication. Required permission: `LOCK`. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock before locking. If omitted, the SDK uses the last server time if available. | | `operationId` | `Int?` | No | Operation ID for tracking the BLE command in activity logs. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected. | | `TimeoutException` | 703 | Operation exceeded the 15-second BLE timeout. | | `GenericException` | 1 | Any other unhandled error. | On iOS, `lock`/`unlock` throw `IgloohomeError` — see [Error Reference](/home/sdk_errors). ### Example ```kotlin try { sdk.lock( deviceId = "IGM401abc123", key = guestKey, ) showSuccess("Door locked") } catch (e: IglooHomeException) { when (e) { is IglooHomeException.BluetoothException -> showError("Please enable Bluetooth") is IglooHomeException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooHomeException.TimeoutException -> showError("Operation timed out, try again") else -> showError("Failed: ${e.message}") } } ``` ```swift do { try await sdk.lock("IGM401abc123", key: guestKey) showSuccess(message: "Door locked") } catch IgloohomeError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch IgloohomeError.connection { showError(message: "unexpected bluetooth connection issue") } catch IgloohomeError.timeout { showError(message: "Operation timed out, try again") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## unlock Unlocks the device via Bluetooth. Optionally sets the lock's internal clock before unlocking. ### Signature ```kotlin suspend fun unlock( deviceId: String, key: String, timeInSeconds: Long? = null, operationId: Int? = null, ) ``` ```swift func unlock( _ deviceId: String, key: String, operationId: Int? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey for BLE authentication. Required permission: `UNLOCK`. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock before unlocking. | | `operationId` | `Int?` | No | Operation ID for tracking the BLE command in activity logs. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected. | | `TimeoutException` | 703 | Operation exceeded the 15-second BLE timeout. | | `GenericException` | 1 | Any other unhandled error. | ### Example ```kotlin try { sdk.unlock( deviceId = "IGM401abc123", key = guestKey, ) showSuccess("Door unlocked") } catch (e: IglooHomeException) { when (e) { is IglooHomeException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooHomeException.TimeoutException -> showError("Timed out, try again") else -> showError("Failed: ${e.message}") } } ``` ```swift do { try await sdk.unlock("IGM401abc123", key: guestKey) showSuccess(message: "Door unlocked") } catch IgloohomeError.connection { showError(message: "unexpected bluetooth connection issue") } catch IgloohomeError.timeout { showError(message: "Timed out, try again") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## disconnect Closes the BLE connection to a specific lock. Safe to call even if not connected. ### Signature ```kotlin suspend fun disconnect(deviceId: String) ``` ```swift func disconnect(_ deviceId: String) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock to disconnect from. | ### Example ```kotlin sdk.disconnect("IGM401abc123") ``` ```swift try? await sdk.disconnect("IGM401abc123") ``` --- ## disconnectAll Closes all active BLE connections. ### Signature ```kotlin suspend fun disconnectAll() ``` ### Example ```kotlin sdk.disconnectAll() ``` ### Notes - The default BLE timeout is 15 seconds for lock/unlock operations. - The SDK automatically connects to the lock when `lock()` or `unlock()` is called — there is no separate `connect()` method. - Always call `disconnect()` or `disconnectAll()` when done to free BLE resources. On iOS, call `disconnect(_:)` for each device you connected to. - If the lock disconnects during an operation, `ConnectionException` (Android) / `IgloohomeError.connection` (iOS) is thrown immediately. ### FILE: home/sdk_pairing Title: Pairing Category: BLE SDK ---------------------------------------- # Pairing Register and remove igloohome devices from your account. --- ## pair Pairs an Igloo lock via Bluetooth and registers it on the server. Protocol (G2/G3) is auto-detected from the lock's firmware. The SDK orchestrates the full flow: timezone resolution, DST data fetch, BLE handshake, server registration, and commit. If commit fails after server registration, the device is automatically rolled back (deleted from server). ### Signature ```kotlin suspend fun pair( deviceId: String, name: String, propertyIds: List, accessToken: String, ): PairResult ``` ```swift func pair( deviceId: String, lockName: String, propertyIds: [String], accessToken token: String ) async throws -> PairResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock (e.g. `"SP2X01abc123"`). | | `name` | `String` | Yes | User-assigned name for the lock. | | `propertyIds` | `List` | Yes | IDs of properties to assign this lock to. Must be non-empty. All properties must share the same timezone. | | `accessToken` | `String` | Yes | OAuth access token for server calls. | ### Return Type ```kotlin data class PairResult( val type: String, val deviceId: String, val name: String, val pairedAt: String, val properties: List, ) data class PairResultProperty( val name: String, val timezone: String, val id: String, ) ``` ```swift public struct PairResult: Sendable { public let type: String public let bluetoothDeviceName: String public let name: String public let pairedAt: String public let properties: [Property] } public struct Property: Sendable { public let name: String public let timezone: String public let id: String } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected during pairing. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `DevicePairedException` | 910 | Device is already paired (detected server-side or via BLE scan). | | `ApiException` | varies | Server API call failed. | | `GenericException` | 1 | Property IDs empty, timezone conflict, or unsupported protocol. | ### Example ```kotlin try { val result = sdk.pair( deviceId = "SP2X01abc123", name = "Front Door Lock", propertyIds = listOf("prop-123"), accessToken = accessToken, ) println("Paired: ${result.name} at ${result.pairedAt}") println("Properties: ${result.properties.map { it.name }}") } catch (e: IglooHomeException.DevicePairedException) { showError("This lock is already paired") } catch (e: IglooHomeException.HasLinkedDeviceException) { showError("Unlink accessories first: ${e.linkedDeviceIds}") } catch (e: IglooHomeException) { showError("Pairing failed: ${e.message}") } ``` ```swift do { let result = try await sdk.pair( deviceId: "SP2X01abc123", lockName: "Front Door Lock", propertyIds: ["prop-123"], accessToken: accessToken) print("Paired: \(result.name) at \(result.pairedAt)") print("Properties: \(result.properties.map(\.name))") } catch IgloohomeError.devicePaired { showError(message: "This lock is already paired") } catch IgloohomeError.hasLinkedDevice(let linkedDeviceIds) { showError(message: "Unlink accessories first: \(linkedDeviceIds)") } catch { showError(message: "Pairing failed: \(error.localizedDescription)") } ``` ### Notes - The device must be in pairing mode (unpaired state). - All `propertyIds` must share the same timezone — the SDK fetches DST data based on this shared timezone. - G3 locks (protocol version 2) use certificate-based pairing with an additional server round-trip. - If the BLE commit step fails after server registration, the SDK automatically deletes the server record. --- ## unpair Unpairs a lock via Bluetooth and removes it from the server. The SDK checks for linked devices first. If the lock has linked accessories (keypads, fobs, bridges), it throws `HasLinkedDeviceException` — you must unlink them first. ### Signature ```kotlin suspend fun unpair( deviceId: String, accessToken: String, ) ``` ```swift func unpair( deviceId: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `accessToken` | `String` | Yes | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during unpair. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `HasLinkedDeviceException` | 910 | Device has linked accessories. Contains `linkedDeviceIds: List`. | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { sdk.unpair( deviceId = "SP2X01abc123", accessToken = accessToken, ) showSuccess("Lock removed") } catch (e: IglooHomeException.HasLinkedDeviceException) { showError("Unlink these devices first: ${e.linkedDeviceIds}") } catch (e: IglooHomeException) { showError("Unpair failed: ${e.message}") } ``` ```swift do { try await sdk.unpair(deviceId: "SP2X01abc123", accessToken: accessToken) showSuccess(message: "Lock removed") } catch IgloohomeError.hasLinkedDevice(let linkedDeviceIds) { showError(message: "Unlink these devices first: \(linkedDeviceIds)") } catch { showError(message: "Unpair failed: \(error.localizedDescription)") } ``` ### Notes - The SDK automatically fetches the correct key for BLE unpair (eKey for G3, admin key for G2). Unlike `lock`/`unlock`, `unpair` on both platforms does not take a `key` parameter — it is resolved internally. - Server deletion retries up to 3 times with 5-second delays on failure. - After unpair, all access credentials (PINs, keycards, fingerprints) are removed. --- ## setWifiConfig Configures WiFi on an Igloo Bridge device via Bluetooth. After sending WiFi credentials via BLE, the SDK generates a CSR on the bridge and polls the server until the bridge is provisioned and online. ### Signature ```kotlin suspend fun setWifiConfig( deviceId: String, key: String, ssid: String, networkPassword: String, accessToken: String, ) ``` ```swift func setWifiConfig( deviceId: String, key: String, ssid: String, networkPassword: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the bridge (e.g. `"EB1X01abc123"`). | | `key` | `String` | Yes | Admin key for BLE authentication. | | `ssid` | `String` | Yes | WiFi network SSID. | | `networkPassword` | `String` | Yes | WiFi network password. | | `accessToken` | `String` | Yes | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during setup. | | `TimeoutException` | 703 | Bridge was not provisioned or online after all polling retries (15s, 30s, 45s). | ### Example ```kotlin try { sdk.setWifiConfig( deviceId = "EB1X01abc123", key = adminKey, ssid = "Office-WiFi", networkPassword = "password123", accessToken = accessToken, ) showSuccess("Bridge WiFi configured and online") } catch (e: IglooHomeException.TimeoutException) { showError("Bridge did not come online — check WiFi credentials") } catch (e: IglooHomeException) { showError("WiFi setup failed: ${e.message}") } ``` ```swift do { try await sdk.setWifiConfig( deviceId: "EB1X01abc123", key: adminKey, ssid: "Office-WiFi", networkPassword: "password123", accessToken: accessToken) showSuccess(message: "Bridge WiFi configured and online") } catch IgloohomeError.timeout { showError(message: "Bridge did not come online — check WiFi credentials") } catch { showError(message: "WiFi setup failed: \(error.localizedDescription)") } ``` ### Notes - The SDK sends WiFi credentials + MQTT broker config to the bridge in a single BLE command. - After the BLE write, the SDK generates a private key CSR on the bridge. - The SDK then polls the server at 15s, 30s, and 45s intervals until `isProvisioned` and `isOnline` are both `true`. - If the bridge doesn't come online after all retries, `TimeoutException` (Android) / `IgloohomeError.timeout` (iOS) is thrown. ### FILE: home/sdk_pin Title: PIN Management Category: BLE SDK ---------------------------------------- # PIN Management Create and delete custom PINs on igloohome locks. --- ## createPin Creates a custom PIN on the lock via Bluetooth and registers it on the server. The SDK writes the PIN to lock hardware first, then registers on the server. If server registration fails, the SDK automatically deletes the PIN from the lock. ### Signature ```kotlin suspend fun createPin( deviceId: String, key: String, pin: String, pinType: PinType, name: String, startTimeInSeconds: Long? = null, endTimeInSeconds: Long? = null, accessToken: String, ): CreatePinResponse ``` ```swift func createPin( deviceId: String, key: String, name: String, pin: String, pinType: PinType, startDate: Date?, endDate: Date?, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `CREATE_PIN` permission. | | `pin` | `String` | Yes | PIN code (numeric, 1–12 digits). | | `pinType` | `PinType` | Yes | PIN type: `ONETIME`, `PERMANENT`, or `DURATION`. | | `name` | `String` | Yes | User-assigned name for the PIN. | | `startTimeInSeconds` | `Long?` | Duration only | Start time in epoch seconds. Required for `DURATION` type. | | `endTimeInSeconds` | `Long?` | Duration only | End time in epoch seconds. Required for `DURATION` type. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Supporting Types ```kotlin enum class PinType { ONETIME, PERMANENT, DURATION, } ``` ```swift public enum PinType: String, Sendable { case otp = "otp" case permanent = "permanent" case duration = "duration" } ``` ### Return Type ```kotlin data class CreatePinResponse( val accessId: String, val name: String, val pin: String, val pinType: String, val startDateTime: String?, val endDateTime: String?, ) ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during PIN creation. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `DuplicatePinException` | 880 | PIN already exists on the lock. | | `LockStorageFullException` | 912 | No storage for more PINs on the lock. | | `ApiException` | varies | Server registration failed (PIN rolled back from lock). | ### Example ```kotlin try { val result = sdk.createPin( deviceId = "IGM401abc123", key = guestKey, pin = "123456", pinType = PinType.PERMANENT, name = "Front Door PIN", accessToken = accessToken, ) println("PIN created: ${result.accessId}") } catch (e: IglooHomeException.DuplicatePinException) { showError("This PIN already exists on the lock") } catch (e: IglooHomeException.LockStorageFullException) { showError("Lock is full — delete a PIN first") } catch (e: IglooHomeException) { showError("Failed: ${e.message}") } ``` ```swift do { try await sdk.createPin( deviceId: "IGM401abc123", key: guestKey, name: "Front Door PIN", pin: "123456", pinType: .permanent, startDate: nil, endDate: nil, accessToken: accessToken) showSuccess(message: "PIN created") } catch IgloohomeError.duplicatePin { showError(message: "This PIN already exists on the lock") } catch IgloohomeError.lockStorageFull { showError(message: "Lock is full — delete a PIN first") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## deletePin Deletes a PIN from the lock and removes the server record. The SDK fetches the PIN value from the server, removes it from lock hardware via BLE, then deletes the server record. If the PIN is already removed from the lock (BLE error 890), the SDK still deletes the server record. ### Signature ```kotlin suspend fun deletePin( deviceId: String, key: String, accessId: String, accessToken: String, ) ``` ```swift func deletePin( deviceId: String, key: String, accessId: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_PIN` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the PIN to delete. | | `accessToken` | `String` | Yes | Access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deletePin( deviceId = "IGM401abc123", key = guestKey, accessId = "access-123", accessToken = accessToken, ) showSuccess("PIN deleted") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Failed: ${e.message}") } ``` ```swift do { try await sdk.deletePin( deviceId: "IGM401abc123", key: guestKey, accessId: "access-123", accessToken: accessToken) showSuccess(message: "PIN deleted") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Server deletion retries up to 3 times with 1-second delays on failure. - `PinNotFoundException` (890, Android) / `IgloohomeError.pinNotFound` (iOS) during BLE delete is treated as non-fatal — the SDK continues to delete the server record. ### FILE: home/sdk_scanning Title: Scanning Category: BLE SDK ---------------------------------------- # Scanning Discover nearby igloohome smart locks via Bluetooth. --- ## scanDevice Scans for nearby Igloo locks via Bluetooth. Emits a `ScanResult` for each discovered device. The scan runs for as long as the Flow has active collectors. ### Signature ```kotlin fun scanDevice(): Flow ``` ```swift func scansLock() -> AsyncThrowingStream func stopScan() ``` ### Return Type ```kotlin data class ScanResult( val deviceId: String, val isPaired: Boolean, val isActive: Boolean, val rssi: Int, ) ``` ```swift public struct ScanResult: Identifiable, Hashable, Sendable { public var id: String { bluetoothDeviceName } public var bluetoothDeviceName: String public var isActive: Bool public var isPaired: Bool public var isG3: Bool } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | ### Example ```kotlin val scanJob = viewModelScope.launch { try { sdk.scanDevice().collect { result -> println("Found: ${result.deviceId}") println(" paired=${result.isPaired}, active=${result.isActive}, rssi=${result.rssi}") } } catch (e: IglooHomeException.BluetoothException) { showError("Please enable Bluetooth") } catch (e: IglooHomeException) { showError("Scan failed: ${e.message}") } } // Stop scanning scanJob.cancel() ``` ```swift let scanTask = Task { do { for try await result in sdk.scansLock() { print("Found: \(result.bluetoothDeviceName)") print(" paired=\(result.isPaired), active=\(result.isActive), type=\(result.type)") } } catch IgloohomeError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch { showError(message: "Scan failed: \(error.localizedDescription)") } } // Stop scanning sdk.stopScan() scanTask.cancel() ``` ### Notes - The scan runs indefinitely until the collecting coroutine is cancelled (Android) or `stopScan()` is called and the enclosing `Task` is cancelled (iOS). - Ensure BLE permissions are granted before calling `scanDevice()` / `scansLock()`. On iOS this means `NSBluetoothAlwaysUsageDescription` is set in `Info.plist` — there is no runtime permission prompt to request explicitly. - The `deviceId` / `bluetoothDeviceName` prefix indicates the lock type (e.g. `IGM4` = mortise lock, `SP2X` = smart padlock, `EB1` = bridge). On iOS this is also exposed structurally via `ScanResult.type: LockType`. - Unpaired devices (`isPaired = false`) can be registered via `pair()`. - Filter by `rssi` to find nearby devices — typical threshold is `-70` for "close enough to connect". Not available on iOS (no `rssi` field). ### FILE: home/sdk_sync Title: Sync Category: BLE SDK ---------------------------------------- # Sync Synchronize device state via Bluetooth. --- ## sync Syncs the lock via Bluetooth: sets the lock's internal clock, retrieves battery level, uploads activity logs, and updates the device on the server. ### Signature ```kotlin suspend fun sync( deviceId: String, key: String, timeInSeconds: Long? = null, getDeviceToken: String, storeLogsToken: String, updateDeviceToken: String, operationId: Int? = null, ): SyncResult ``` ```swift func sync( _ deviceId: String, key: String, getDeviceToken deviceToken: String, storeLogsToken storeToken: String, updateDeviceToken updateToken: String, timeInSeconds time: Int? = nil ) async throws -> SyncResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `GET_TIME`, `SET_TIME`, `GET_BATTERY_LEVEL`, `GET_LOGS` permissions. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock. If omitted, SDK uses last server time. | | `getDeviceToken` | `String` | Yes | OAuth access token for fetching device info. | | `storeLogsToken` | `String` | Yes | OAuth access token for uploading activity logs. | | `updateDeviceToken` | `String` | Yes | OAuth access token for updating device state on the server. | | `operationId` | `Int?` | No | Operation ID for tracking. | ### Return Type ```kotlin data class SyncResult(val batteryLevel: Int) ``` ```swift public struct SyncResult: Sendable { public let batteryLevel: Int } ``` | Field | Description | |-------|-------------| | `batteryLevel` | Battery level as percentage (0-100). | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during sync. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Activity log upload or device update failed. | ### Example ```kotlin try { val result = sdk.sync( deviceId = "IGM401abc123", key = guestKey, getDeviceToken = accessToken, storeLogsToken = accessToken, updateDeviceToken = accessToken, ) println("Battery: ${result.batteryLevel}%") } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooHomeException) { showError("Sync failed: ${e.message}") } ``` ```swift do { let result = try await sdk.sync( "IGM401abc123", key: guestKey, getDeviceToken: accessToken, storeLogsToken: accessToken, updateDeviceToken: accessToken) print("Battery: \(result.batteryLevel)%") } catch { showError(message: "Sync failed: \(error.localizedDescription)") } ``` ### Notes - Sync reads all pending activity logs from the lock in a loop until no more remain. - Logs are stored in a local database first, then batch-uploaded to the server. - If `timeInSeconds` is omitted and no cached server time is available, the lock time is not updated. - The three token parameters (`getDeviceToken`, `storeLogsToken`, `updateDeviceToken`) can be the same token if it has all required scopes, or different tokens with scoped permissions. ### FILE: works/sdk_best_practices Title: Best Practices Category: BLE SDK ---------------------------------------- # Best Practices Patterns for building reliable apps with the iglooworks BLE SDK. --- ## Architecture ``` ┌──────────────┐ │ Your App │ └──────┬───────┘ │ IglooPlugin API (suspend / Flow) ┌──────▼───────┐ │ IglooWorks │──── REST API ────► Igloo Server │ SDK │ └──────┬───────┘ │ BLE ┌──────▼───────┐ │ Smart Lock │ └──────────────┘ ``` The SDK orchestrates BLE and server operations together. For example, `createPin()` writes the PIN to lock hardware via BLE, registers it on the server, and rolls back if the server call fails — all in one call. --- ## Singleton Pattern Only one `IglooPlugin` instance should exist at a time. Multiple instances hold separate connection state and will cause undefined BLE behavior. ```kotlin // Application-scoped singleton object SdkProvider { lateinit var sdk: IglooPlugin private set fun init(context: Context, apiKey: String) { sdk = IglooPlugin(context.applicationContext, apiKey) } } ``` ```swift // App-scoped singleton. IglooPlugin is @MainActor and holds no context — // pick whichever initializer matches your auth mode. @MainActor enum SdkProvider { static let sdk = IglooPlugin(apiKey: "your-api-key") } ``` --- ## Connection Management - **One operation at a time.** BLE operations are serialized internally per device — don't fire multiple lock/unlock calls concurrently on the same device. - **Disconnect when done.** Call `disconnect(deviceId)` after your operation completes to free BLE resources. - **The SDK connects automatically.** There is no separate `connect()` method — `lock()`, `sync()`, `pair()` etc. all connect internally. ```kotlin try { sdk.lock(deviceId, key) } finally { sdk.disconnect(deviceId) } ``` ```swift // Swift has no try/finally — use `defer` instead, which runs when the // enclosing scope exits whether `lock` throws or not. defer { Task { try? await sdk.disconnect(deviceId) } } try await sdk.lock(deviceId, key: key) ``` --- ## Swift Concurrency Patterns All BLE operations are `async throws` functions or return `AsyncStream`/`AsyncThrowingStream`. Use structured concurrency (`Task`, `async let`, task cancellation) in place of Kotlin's coroutine scopes. ### Async Functions ```kotlin // In a ViewModel viewModelScope.launch { try { sdk.lock(deviceId, key) _uiState.value = UiState.Success } catch (e: IglooWorksException) { _uiState.value = UiState.Error(e.message) } } ``` ```swift // In an @Observable / ObservableObject view model Task { do { try await sdk.lock(deviceId, key: key) uiState = .success } catch { uiState = .error(error.localizedDescription) } } ``` ### Stream Collection ```kotlin // Scanning — cancel when no longer needed private var scanJob: Job? = null fun startScan() { scanJob = viewModelScope.launch { sdk.scanDevice().collect { result -> _devices.value += result } } } fun stopScan() { scanJob?.cancel() } ``` ```swift // Scanning — cancel when no longer needed private var scanTask: Task? func startScan() { scanTask = Task { do { for try await result in sdk.scansLock() { devices.append(result) } } catch { // Handle scan error } } } func stopScan() { sdk.stopScan() scanTask?.cancel() } ``` ### Stream with View Lifecycle ```kotlin // In a Fragment/Activity lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { sdk.addFingerprint(deviceId, key, name).collect { event -> // Handle scan events } } } ``` ```swift // In a SwiftUI View, tied to the view's lifetime .task { for await event in await sdk.addFingerprint(deviceId: deviceId, key: key, name: name) { // Handle scan events } } ``` --- ## Error Handling Catch at the UI layer, not deep in the call stack. ```kotlin viewModelScope.launch { try { sdk.pair(deviceId, name, propertyIds) _uiState.value = UiState.Paired } catch (e: IglooWorksException) { _uiState.value = when (e) { is IglooWorksException.DevicePairedException -> UiState.Error("Already paired") is IglooWorksException.HasLinkedDeviceException -> UiState.Error("Unlink ${e.linkedDeviceIds.size} accessories first") is IglooWorksException.ConnectionException -> UiState.Retryable("unexpected bluetooth connection issue") is IglooWorksException.TimeoutException -> UiState.Retryable("Connection lost — try again") else -> UiState.Error(e.message ?: "Unknown error") } } } ``` ```swift Task { do { try await sdk.pair(deviceId: deviceId, lockName: name, propertyIds: propertyIds) uiState = .paired } catch IglooworksError.devicePaired { uiState = .error("Already paired") } catch IglooworksError.hasLinkedDevice(let linkedDeviceIds) { uiState = .error("Unlink \(linkedDeviceIds.count) accessories first") } catch LockManagerError.deviceDisconnected { uiState = .retryable("unexpected bluetooth connection issue") } catch IglooworksError.timeout, LockManagerError.LockTimeoutError { uiState = .retryable("Connection lost — try again") } catch { uiState = .error(error.localizedDescription) } } ``` **Retry only transient errors:** `TimeoutException`, `ConnectionException`, and `ApiException` with 5xx status (Android); `IglooworksError.timeout`, `LockManagerError.LockTimeoutError`/`.deviceDisconnected`, and `IglooworksError.api` with 5xx status (iOS). Terminal errors like `DevicePairedException`/`IglooworksError.devicePaired` or `LockStorageFullException`/`IglooworksError.lockStorageFull` require user action. --- ## Recommended Operation Sequences ### First-Time Device Setup 1. `scanDevice()` (Android) / `scansLock()` (iOS) — find the lock 2. `pair()` — register on account 3. `calibrate()` — tune motor direction 4. `sync()` — set clock and read battery 5. `setWifiConfig()` — if bridge, configure WiFi ### Regular Operations 1. `lock()` / `unlock()` — control the lock 2. `sync()` — periodically sync time and logs 3. `syncJob()` (Android) / `syncJobs()` (iOS) — execute queued PIN/key jobs ### Access Management 1. `createPin()` / `addKeycard()` / `addFingerprint()` — add access 2. `deletePin()` / `deleteKeycard()` / `deleteFingerprint()` — remove access ### Firmware Update 1. `checkFirmwareUpdate()` — check for updates 2. `performDfu()` — apply update (keep screen on) 3. `sync()` — verify device state after update ### Device Removal 1. `unlink()` — remove all accessories first 2. `unpair()` — remove the lock --- ## Troubleshooting | Problem | Cause | Solution | |---------|-------|----------| | `BluetoothException` on every call | Bluetooth disabled or permissions not granted | Check `BluetoothAdapter.isEnabled` and request runtime permissions | | `ConnectionException` frequently | Device out of BLE range | Move within 2–3 meters of the lock | | `TimeoutException` on first call | Lock in deep sleep | Retry — first connection wakes the lock | | `DevicePairedException` during pair | Lock already registered | Check server if you own it; factory reset if transferring | | `HasLinkedDeviceException` during unpair | Accessories still linked | Call `unlink()` for each accessory first | | `DuplicatePinException` | Same PIN exists on lock | Choose a different PIN code | | `LockStorageFullException` | Lock PIN/card slots exhausted | Delete existing access before adding new ones | | `BatteryLowException` during DFU | Battery below threshold | Charge or replace batteries before DFU | | Multiple `IglooPlugin` instances | Creating SDK in Activity/Fragment | Use application-scoped singleton | | BLE operations fail silently | RxJava undeliverable exceptions | SDK handles these internally — upgrade if on old version | The same table for iOS: | Problem | Cause | Solution | |---------|-------|----------| | `LockManagerError.bluetoothIsTurnedOff` on every call | Bluetooth disabled, or `NSBluetoothAlwaysUsageDescription` missing from `Info.plist` | Enable Bluetooth; verify the Info.plist key is present | | `LockManagerError.deviceDisconnected` frequently | Device out of BLE range | Move within 2–3 meters of the lock | | `LockManagerError.LockTimeoutError` / `IglooworksError.timeout` on first call | Lock in deep sleep | Retry — first connection wakes the lock | | `IglooworksError.devicePaired` / `LockManagerError.deviceAlreadyPaired` during pair | Lock already registered | Check server if you own it; factory reset if transferring | | `IglooworksError.hasLinkedDevice` during unpair | Accessories still linked | Call `unlink()` for each accessory first | | `IglooworksError.duplicatePin` | Same PIN exists on lock | Choose a different PIN code | | `IglooworksError.lockStorageFull` | Lock PIN/card slots exhausted | Delete existing access before adding new ones | | `IglooworksError.batteryLow` during DFU | Battery below threshold | Charge or replace batteries before DFU | | Testing on Simulator does nothing | Bluetooth is unavailable in the iOS Simulator | Test on a physical device | | `401` from an OAuth-mode `IglooPlugin()` call | No `accessToken`/`...Token` argument passed for that call | Pass the relevant token — required per-call under OAuth mode, unlike API Key mode | ### FILE: works/sdk_calibration Title: Calibration Category: BLE SDK ---------------------------------------- # Calibration Calibrate iglooworks locks to tune motor direction and door settings. --- ## calibrate Calibrates a lock via Bluetooth. Lock type is resolved from the `deviceId` prefix and routes to the appropriate BLE flow. Returns a `Flow` — non-OE1 locks emit only `Complete`, OE1 emits interactive steps requiring user signals. After BLE calibration succeeds, the SDK updates the server with the calibration result. ### Signature ```kotlin fun calibrate( deviceId: String, key: String, lockingDirection: LockingDirection? = null, signals: ReceiveChannel? = null, accessToken: String? = null, ): Flow ``` ```swift func calibrate( deviceId: String, key: String, lockingDirection: LockingDirection? = nil, signals: AsyncStream? = nil, accessToken token: String ) async throws -> AsyncThrowingStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `CALIBRATE` and `SET_BRIGHTNESS` permissions. | | `lockingDirection` | `LockingDirection?` | IGB4 only | Locking direction. Required for IGB4 locks, ignored for others. | | `signals` | `ReceiveChannel?` | OE1 only | Channel for interactive OE1 flow. Required for OE1, ignored for others. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type — CalibrationStep ```kotlin sealed class CalibrationStep { /** OE1: User should open door ajar. Send CalibrationSignal.Proceed to continue. */ object OpenPosition : CalibrationStep() /** OE1: User should close door. Send CalibrationSignal.Proceed to continue. */ object ClosePosition : CalibrationStep() /** OE1: User should select locking direction and door type. Send DoorSettingsSelected. */ object DoorSettings : CalibrationStep() /** All BLE steps complete. */ data class Complete( val lockingDirection: LockingDirection, val cylinderRotation: CylinderRotation?, val unlockHoldEnabled: Boolean?, ) : CalibrationStep() } ``` ```swift public enum CalibrationStep: Equatable, Sendable { case openPosition case closePosition case doorSettings case complete(CalibrateResult) } public struct CalibrateResult: Sendable { public let deviceId: String public let lockingDirection: LockingDirection public let calibratedAt: String public let cylinderRotation: CylinderRotation? } ``` ### Supporting Types ```kotlin enum class LockingDirection { LEFT_HANDED, RIGHT_HANDED, UNKNOWN, } enum class CylinderRotation { SINGLE, DOUBLE, } sealed class CalibrationSignal { /** User is ready — proceed with current step. */ object Proceed : CalibrationSignal() /** User has selected door settings. */ data class DoorSettingsSelected( val lockingDirection: LockingDirection, val doorType: DoorType, ) : CalibrationSignal() } enum class DoorType { /** Both sides have a handle — unlockHoldEnabled = false */ BOTH_SIDES_HAVE_HANDLE, /** One side has no handle — unlockHoldEnabled = true */ ONE_SIDE_NO_HANDLE, } ``` ```swift public enum LockingDirection: String, Sendable { case leftHanded = "left" case rightHanded = "right" } public enum CylinderRotation: Sendable { case single case double } public enum CalibrationSignal: Sendable { case proceed case doorSettingsSelected(lockingDirection: LockingDirection, doorType: DoorType) } public enum DoorType: Sendable { case bothSidesHaveHandle case oneSideNoHandle } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during calibration. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `InvalidLockingDirectionException` | 901 | Auto-detected locking direction is ambiguous. | | `CalibrationFailedException` | 902 | BLE calibration command failed. | | `ApiException` | varies | Server calibration update failed. | ### Example — Standard Lock (DAX, DBX) ```kotlin try { sdk.calibrate( deviceId = "DBX101abc123", key = guestKey, ).collect { step -> when (step) { is CalibrationStep.Complete -> { showSuccess("Calibration complete: ${step.lockingDirection}") } else -> { /* Only OE1 emits intermediate steps */ } } } } catch (e: IglooWorksException.CalibrationFailedException) { showError("Calibration failed — try again") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException) { showError("Calibration failed: ${e.message}") } ``` ```swift do { let steps = try await sdk.calibrate(deviceId: "DBX101abc123", key: guestKey, accessToken: accessToken) for try await step in steps { switch step { case let .complete(result): showSuccess(message: "Calibration complete: \(result.lockingDirection.rawValue)") default: break // Only OE1 emits intermediate steps } } } catch LockManagerError.lockCalibrationFailed { showError(message: "Calibration failed — try again") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Example — IGB4 Lock (requires lockingDirection) ```kotlin try { sdk.calibrate( deviceId = "IGB403fe3b9d", key = guestKey, lockingDirection = LockingDirection.LEFT_HANDED, ).collect { step -> when (step) { is CalibrationStep.Complete -> showSuccess("Calibrated: ${step.lockingDirection}") else -> {} } } } catch (e: IglooWorksException.InvalidLockingDirectionException) { showError("Could not determine locking direction") } catch (e: IglooWorksException.CalibrationFailedException) { showError("Calibration failed — try again") } catch (e: IglooWorksException) { showError("Calibration failed: ${e.message}") } ``` ```swift do { let steps = try await sdk.calibrate( deviceId: "IGB403fe3b9d", key: guestKey, lockingDirection: .leftHanded, accessToken: accessToken) for try await step in steps { switch step { case let .complete(result): showSuccess(message: "Calibrated: \(result.lockingDirection.rawValue)") default: break } } } catch IglooworksError.invalidLockingDirection { showError(message: "Could not determine locking direction") } catch LockManagerError.lockCalibrationFailed { showError(message: "Calibration failed — try again") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Example — OE1 Lock (interactive flow) ```kotlin val signalChannel = Channel() // Collect in one coroutine launch { try { sdk.calibrate( deviceId = "OE1X01abc123", key = guestKey, signals = signalChannel, ).collect { step -> when (step) { is CalibrationStep.OpenPosition -> { showPrompt("Open the door ajar, then tap Continue") // User taps Continue button: signalChannel.send(CalibrationSignal.Proceed) } is CalibrationStep.ClosePosition -> { showPrompt("Close the door, then tap Continue") signalChannel.send(CalibrationSignal.Proceed) } is CalibrationStep.DoorSettings -> { showPrompt("Select locking direction and door type") signalChannel.send( CalibrationSignal.DoorSettingsSelected( lockingDirection = LockingDirection.LEFT_HANDED, doorType = DoorType.ONE_SIDE_NO_HANDLE, ) ) } is CalibrationStep.Complete -> { showSuccess("Calibration complete") signalChannel.close() } } } } catch (e: IglooWorksException.CalibrationFailedException) { showError("Calibration failed — try again") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException) { showError("Calibration failed: ${e.message}") } } ``` ```swift let (signalStream, signalContinuation) = AsyncStream.makeStream() do { let steps = try await sdk.calibrate( deviceId: "OE1X01abc123", key: guestKey, signals: signalStream, accessToken: accessToken) for try await step in steps { switch step { case .openPosition: showPrompt(message: "Open the door ajar, then proceed") signalContinuation.yield(.proceed) case .closePosition: showPrompt(message: "Close the door, then proceed") signalContinuation.yield(.proceed) case .doorSettings: showPrompt(message: "Select locking direction and door type, then confirm") signalContinuation.yield( .doorSettingsSelected(lockingDirection: .leftHanded, doorType: .oneSideNoHandle)) case let .complete(result): showSuccess(message: "Calibrated '\(result.deviceId)' — \(result.lockingDirection.rawValue)") signalContinuation.finish() } } } catch LockManagerError.lockCalibrationFailed { showError(message: "Calibration failed — try again") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch { showError(message: "Calibration failed: \(error.localizedDescription)") } ``` ### Notes - The SDK disconnects from the lock automatically after calibration completes (success or failure). - Non-OE1 locks auto-detect locking direction during calibration (except IGB4 which requires it as a parameter). - OE1 locks require an interactive flow: the user positions the door and selects settings at each step. - After BLE calibration, the SDK sends the result to the server via PATCH `/devices/{deviceId}`. - A `deviceId` prefix that matches no supported model (not `IGB4`, `DAX`, `DBX` or `OE1`) throws `IglooworksError.calibrationFailed` immediately, before any Bluetooth command is sent. The same case also covers a DAX/DBX calibration timeout and an unrecognised rotation or locking-direction result from the lock. ### FILE: works/sdk_errors Title: Error Reference Category: BLE SDK ---------------------------------------- # Error Reference All exceptions thrown by the iglooworks BLE SDK. --- ## Exception Hierarchy All SDK exceptions extend `IglooWorksException`, a sealed class with `status`, `source`, and `message` properties. ```kotlin sealed class IglooWorksException( val status: Int, val source: Throwable? = null, override val message: String? = null, ) : Exception(message, source) ``` On iOS, errors surface as one of two separate `Error` enums depending on which layer failed: `IglooworksError` (orchestration/server errors — `createPin`, `pair`, `link`, DFU, etc.) or `LockManagerError` (raw Bluetooth-layer errors — `lock`, `unlock`, `scan`, etc.). A given method may throw either type, so always end a `catch` chain with a bare `catch { }` to be safe. See per-method pages for the type(s) each one throws in practice. ```swift public enum IglooworksError: Error { case bluetooth(source: Error?, message: String?) case connection(source: Error?, message: String?) case timeout(source: Error? = nil, message: String? = nil) case duplicatePin(source: Error? = nil, message: String? = nil) case pinNotFound(source: Error? = nil, message: String? = nil) case lockUidNotFound(source: Error? = nil, message: String? = nil) case bridgeOffline(source: Error? = nil, message: String? = nil) case invalidLockingDirection(source: Error? = nil, message: String? = nil) case calibrationFailed(source: Error? = nil, message: String? = nil) case lockStorageFull(source: Error? = nil, message: String? = nil) case maxScanAttempts(source: Error? = nil, message: String? = nil) case missingUid(source: Error? = nil, message: String? = nil) case batteryLow(source: Error? = nil, message: String? = nil) case dfuStateNotReady(source: Error? = nil, message: String? = nil) case dfuJobFailed(source: Error? = nil, message: String? = nil) case dfuJobExpired(source: Error? = nil, message: String? = nil) case devicePaired case hasLinkedDevice(linkedDeviceIds: [String]) case api(status: Int, source: Error? = nil, message: String? = nil) case dfuLibrary(status: Int, source: Error? = nil, message: String? = nil) case genericError(_ status: Int, source: Error? = nil, message: String? = nil) } // Conforms to LocalizedError — use `error.localizedDescription` for a formatted message. ``` `IglooworksError.bluetooth` is a catch-all for a Bluetooth-layer failure with no closer match among the other cases (usually a low-level transport error, or an unsupported calibration model). Bluetooth-off, no-lock-found and pairing failures are raised as `LockManagerError` instead — see below. ```swift public enum LockManagerError: Error, Sendable { case noLockFound case bluetoothIsTurnedOff case invalidPairingData case pairingMissingCertificate case pairingFailed(String?) case invalidPairingFlow(String?) case deviceDisconnected case invalidKeycard case invalidCardUid case lockCalibrationFailed case invalidFirmwareVersion case invalidPinType case deviceAlreadyPaired case dfuStateNotReady case lockConnectionError(String?) case LockGenericFailureError(String?) case LockOperationInProgressError(String?) case LockPinNotFoundError(String?) case LockInvalidMasterPinLengthError(String?) case LockInvalidCustomPinLengthError(String?) case LockInvalidVolumeError(String?) case LockInvalidPinKeyLengthError(String?) case LockInvalidRights(String?) case LockPinOccupiedError(String?) case LockBlacklistGuestKeyNotFoundError(String?) case GenericNotFoundError(String?) case LockStorageError(String?) case LockReadError(String?) case LockTimeoutError(String?) case LockMaxAttemptError(String?) case CRCFailure(String?) case LockVerifyError(String?) case LockNotInSchedulerFailure(String?) case BluetoothError(String?) } // Conforms to LocalizedError — use `error.localizedDescription` for a formatted message. ``` --- ## Error Codes | Exception | Code | Description | |-----------|------|-------------| | `GenericException` | 1 | Catch-all for unhandled errors. | | `ConnectionException` | 12 | BLE connection failed or device disconnected unexpectedly. | | `BridgeOfflineException` | 406 | Bridge device is offline (cloud DFU / link operations). | | `TimeoutException` | 703 | Operation exceeded the time limit. Also used for `DfuJobFailedException` and `DfuJobExpiredException`. | | `DfuJobFailedException` | 703 | Cloud firmware update job failed. | | `DfuJobExpiredException` | 703 | Cloud firmware update job expired before completion. | | `BluetoothException` | 708 | Bluetooth is off or unavailable on the device. | | `DuplicatePinException` | 880 | PIN already exists on the lock. | | `PinNotFoundException` | 890 | PIN not found on the lock. | | `InvalidLockingDirectionException` | 901 | Auto-detected locking direction is ambiguous during calibration. | | `CalibrationFailedException` | 902 | BLE calibration command failed. | | `DevicePairedException` | 910 | Device is already paired. | | `HasLinkedDeviceException` | 910 | Device has linked accessories — unlink them first. Carries `linkedDeviceIds: List`. | | `LockStorageFullException` | 912 | Lock storage exhausted (no room for more PINs, keycards, or fingerprints). | | `MaxScanAttemptsException` | 972 | BLE scan retry limit exceeded. | | `LockUidNotFoundException` | 980 | Device UID not found on the lock (card/fingerprint already removed). | | `DfuStateNotReadyException` | 1003 | Device not ready for firmware update (door open, DFU mode not entered). | | `BatteryLowException` | 1010 | Battery too low for firmware update (< 20%, or < 31% for SP2X/SP2E). | | `DfuLibraryException` | 1100+ | Low-level DFU library error. Status varies. | | `ApiException` | varies | HTTP error from server API. Status matches the HTTP response code. | iOS does not use numeric status codes for most cases — only `IglooworksError.api`, `.dfuLibrary`, and `.genericError` carry a `status: Int`. The rest of the mapping is by case name: | Android exception | iOS equivalent | |---|---| | `GenericException` | `IglooworksError.genericError` | | `ConnectionException` | `LockManagerError.deviceDisconnected` | | `BridgeOfflineException` | `IglooworksError.bridgeOffline` | | `TimeoutException` | `IglooworksError.timeout` or `LockManagerError.LockTimeoutError` (BLE-layer) | | `DfuJobFailedException` | `IglooworksError.dfuJobFailed` | | `DfuJobExpiredException` | `IglooworksError.dfuJobExpired` | | `BluetoothException` | `LockManagerError.bluetoothIsTurnedOff` | | `DuplicatePinException` | `IglooworksError.duplicatePin` | | `PinNotFoundException` | `IglooworksError.pinNotFound` | | `InvalidLockingDirectionException` | `IglooworksError.invalidLockingDirection` | | `CalibrationFailedException` | `IglooworksError.calibrationFailed` / `LockManagerError.lockCalibrationFailed` | | `DevicePairedException` | `IglooworksError.devicePaired` / `LockManagerError.deviceAlreadyPaired` | | `HasLinkedDeviceException` | `IglooworksError.hasLinkedDevice(linkedDeviceIds:)` | | `LockStorageFullException` | `IglooworksError.lockStorageFull` | | `MaxScanAttemptsException` | `IglooworksError.maxScanAttempts` | | `LockUidNotFoundException` | `IglooworksError.lockUidNotFound` | | `DfuStateNotReadyException` | `IglooworksError.dfuStateNotReady` / `LockManagerError.dfuStateNotReady` | | `BatteryLowException` | `IglooworksError.batteryLow` | | `DfuLibraryException` | `IglooworksError.dfuLibrary(status:)` | | `ApiException` | `IglooworksError.api(status:)` | --- ## Error Handling Patterns ### Basic ```kotlin try { sdk.lock(deviceId, key) } catch (e: IglooWorksException) { showError("Error ${e.status}: ${e.message}") } ``` ```swift do { try await sdk.lock(deviceId, key: key) } catch { showError(message: error.localizedDescription) } ``` ### Exhaustive ```kotlin try { sdk.lock(deviceId, key) } catch (e: IglooWorksException) { when (e) { is IglooWorksException.BluetoothException -> showError("Please enable Bluetooth") is IglooWorksException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooWorksException.TimeoutException -> showError("Operation timed out, try again") else -> showError("Error: ${e.message}") } } ``` ```swift do { try await sdk.lock(deviceId, key: key) } catch LockManagerError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch LockManagerError.LockTimeoutError { showError(message: "Operation timed out, try again") } catch { showError(message: "Error: \(error.localizedDescription)") } ``` > Because `lock`/`unlock` throw `LockManagerError` directly (not `IglooworksError`), the exhaustive `catch` list above only needs to match `LockManagerError` cases. Methods that orchestrate a server call too (`pair`, `createPin`, `link`, DFU, …) can throw `IglooworksError` cases as well — add `catch IglooworksError.xxx { }` clauses for those as shown on each method's own page. ### Flow-Based (DFU, Fingerprint) ```kotlin sdk.performDfu(deviceId, key, firmwareUid).collect { event -> when (event) { is DfuEvent.Progress -> updateProgressBar(event.percent) is DfuEvent.Complete -> showSuccess("Updated to ${event.newFirmwareVersion}") is DfuEvent.Failed -> showError("DFU failed: ${event.error.message}") } } ``` ```swift do { let events = try await sdk.performDfu(deviceId: deviceId, key: key, firmwareUid: firmwareUid) for try await event in events { switch event { case let .progress(percent): updateProgressBar(percent) case let .complete(newFirmwareVersion): showSuccess(message: "Updated to \(newFirmwareVersion)") } } } catch { showError(message: "DFU failed: \(error.localizedDescription)") } ``` --- ## Retryable vs Terminal | Type | Exceptions | Recommended Action | |------|-----------|-------------------| | Retryable | `TimeoutException`, `ConnectionException` | Retry the operation (user may need to move closer). | | Retryable | `ApiException` (5xx) | Retry after delay. | | Terminal | `BluetoothException` | Prompt user to enable Bluetooth. | | Terminal | `DevicePairedException` | Device already registered — no action needed. | | Terminal | `HasLinkedDeviceException` | Unlink accessories first using `unlink()`. | | Terminal | `DuplicatePinException` | Choose a different PIN. | | Terminal | `LockStorageFullException` | Delete existing access before adding new ones. | | Terminal | `BatteryLowException` | Wait for battery to charge before DFU. | | Terminal | `CalibrationFailedException` | Hardware issue — retry calibration from scratch. | | Non-fatal | `LockUidNotFoundException`, `PinNotFoundException` | SDK treats these as "already removed" during delete operations. | The same table on iOS, by case name: | Type | iOS cases | Recommended Action | |------|-----------|-------------------| | Retryable | `IglooworksError.timeout`, `LockManagerError.LockTimeoutError`, `LockManagerError.deviceDisconnected` | Retry the operation (user may need to move closer). | | Retryable | `IglooworksError.api` (5xx) | Retry after delay. | | Terminal | `LockManagerError.bluetoothIsTurnedOff` | Prompt user to enable Bluetooth. | | Terminal | `IglooworksError.devicePaired`, `LockManagerError.deviceAlreadyPaired` | Device already registered — no action needed. | | Terminal | `IglooworksError.hasLinkedDevice` | Unlink accessories first using `unlink()`. | | Terminal | `IglooworksError.duplicatePin` | Choose a different PIN. | | Terminal | `IglooworksError.lockStorageFull` | Delete existing access before adding new ones. | | Terminal | `IglooworksError.batteryLow` | Wait for battery to charge before DFU. | | Terminal | `IglooworksError.calibrationFailed`, `LockManagerError.lockCalibrationFailed` | Hardware issue — retry calibration from scratch. | | Non-fatal | `IglooworksError.lockUidNotFound`, `.pinNotFound` | SDK treats these as "already removed" during delete operations. | ### FILE: works/sdk_fingerprint Title: Fingerprint Category: BLE SDK ---------------------------------------- # Fingerprint Enroll and remove fingerprints on iglooworks locks. --- ## addFingerprint Opens a BLE fingerprint registration window and returns a Flow of scan events. The lock requires 3 good reads to enroll a fingerprint (60-second timeout). On successful enrollment, the SDK registers the fingerprint on the server. If server registration fails, the SDK automatically removes the fingerprint from lock hardware. ### Signature ```kotlin fun addFingerprint( deviceId: String, key: String, name: String, accessToken: String? = null, ): Flow ``` ```swift func addFingerprint( deviceId: String, key: String, name: String, accessToken token: String ) async -> AsyncStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `ADD_FINGERPRINT` permission. | | `name` | `String` | Yes | User-assigned name for the fingerprint. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type — FingerprintScanEvent ```kotlin sealed class FingerprintScanEvent { /** Good read recorded. More reads needed. */ data class ScanProgress(val attempt: Int, val total: Int) : FingerprintScanEvent() /** Bad read. Prompt user to try again. */ class ScanError : FingerprintScanEvent() /** Enrollment completed and registered on server. */ data class Completed(val accessId: String, val name: String) : FingerprintScanEvent() /** Enrollment failed. */ data class Failed(val error: IglooWorksException) : FingerprintScanEvent() } ``` ```swift public enum FingerprintScanEvent { case scanProgress(attempt: Int, total: Int) case scanError case completed(accessId: String, name: String) case failed(error: Error) } ``` | Event | Description | |-------|-------------| | `ScanProgress(attempt, total)` | A good fingerprint scan. `attempt` = reads so far, `total` = 3. | | `ScanError` | Bad read — finger not positioned correctly. Prompt to retry. | | `Completed(accessId, name)` | All 3 reads successful. Fingerprint registered on server. | | `Failed(error)` | Enrollment failed (timeout, disconnect, etc.). | ### Example ```kotlin try { sdk.addFingerprint( deviceId = "IGM401abc123", key = guestKey, name = "Right Thumb", ).collect { event -> when (event) { is FingerprintScanEvent.ScanProgress -> showProgress("Scan ${event.attempt}/${event.total}") is FingerprintScanEvent.ScanError -> showWarning("Bad read — try again") is FingerprintScanEvent.Completed -> showSuccess("Fingerprint enrolled: ${event.accessId}") is FingerprintScanEvent.Failed -> { when (event.error) { is IglooWorksException.TimeoutException -> showError("Timed out — place finger on sensor") is IglooWorksException.ConnectionException -> showError("unexpected bluetooth connection issue") else -> showError("Failed: ${event.error.message}") } } } } } catch (e: IglooWorksException.BluetoothException) { showError("Please enable Bluetooth") } catch (e: IglooWorksException) { showError("Fingerprint enrollment failed: ${e.message}") } ``` ```swift let stream = await sdk.addFingerprint( deviceId: "IGM401abc123", key: guestKey, name: "Right Thumb", accessToken: accessToken) for await event in stream { switch event { case let .scanProgress(attempt, total): showProgress(message: "Scanning \(attempt)/\(total) — place the finger on the reader") case .scanError: showWarning(message: "Bad read — try again") case let .completed(accessId, name): showSuccess(message: "Fingerprint '\(name)' enrolled: \(accessId)") case let .failed(error): showError(message: error.localizedDescription) } } ``` ### Notes - The lock requires exactly 3 good reads to enroll a fingerprint. - Bad reads don't count toward the total — the user just tries again. - The entire flow times out after 60 seconds. - Errors are emitted as `Failed` events rather than thrown as exceptions. --- ## deleteFingerprint Removes a fingerprint from the lock and deletes the server record. The SDK fetches the fingerprint UID from the server, removes it from lock hardware via BLE, then deletes the server record. ### Signature ```kotlin suspend fun deleteFingerprint( deviceId: String, key: String, accessId: String, accessToken: String? = null, ) ``` ```swift func deleteFingerprint( deviceId: String, key: String, accessId: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_FINGERPRINT_UID` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the fingerprint to delete. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deleteFingerprint( deviceId = "IGM401abc123", key = guestKey, accessId = "access-789", ) showSuccess("Fingerprint removed") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Delete failed: ${e.message}") } ``` ```swift do { try await sdk.deleteFingerprint( deviceId: "IGM401abc123", key: guestKey, accessId: "access-789", accessToken: accessToken) showSuccess(message: "Fingerprint removed") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Server deletion retries up to 3 times with 1-second delays. - `LockUidNotFoundException` (980, Android) / `IglooworksError.lockUidNotFound` (iOS) during BLE delete is non-fatal — server record is still removed. ### FILE: works/sdk_firmware Title: Firmware Updates Category: BLE SDK ---------------------------------------- # Firmware Updates Check for and perform firmware updates on iglooworks devices. --- ## checkFirmwareUpdate Checks whether a firmware update is available for the device. For BLE locks: reads the current firmware version from the lock over BLE, patches the server, then queries the DFU service. For EB1 bridges: reads the server-stored firmware version (no BLE) and queries the DFU service. ### Signature ```kotlin suspend fun checkFirmwareUpdate( deviceId: String, key: String, accessToken: String? = null, ): FirmwareUpdateInfo ``` ```swift func checkFirmwareUpdate( deviceId: String, key: String ) async throws -> FirmwareUpdateInfo ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name (or device ID for EB1). | | `key` | `String` | Yes | eKey with `GET_FIRMWARE_VERSION` permission. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type ```kotlin data class FirmwareUpdateInfo( val hasUpdate: Boolean, val currentVersion: String, val firmwareUid: String?, val latestFirmwareUid: String?, val firmwareSizeBytes: Long?, ) ``` ```swift public struct FirmwareUpdateInfo { public let hasUpdate: Bool public let currentVersion: String public let firmwareUid: String? public let firmwareSizeBytes: Int? } ``` | Field | Description | |-------|-------------| | `hasUpdate` | `true` if a newer firmware is available. | | `currentVersion` | Current firmware version string on the device. | | `firmwareUid` | UID of the available firmware package. `null` if no update. | | `latestFirmwareUid` | UID of the latest available firmware. `null` if no update. | | `firmwareSizeBytes` | Size of the firmware binary in bytes. `null` if no update. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable (BLE path). | | `ConnectionException` | 12 | Device disconnected during version read. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server API call failed. | Both `checkFirmwareUpdate` and `performDfu` start by scanning for the device, which on iOS throws `LockManagerError.noLockFound` (device did not respond) or `.bluetoothIsTurnedOff` before anything else runs. ### Example ```kotlin try { val info = sdk.checkFirmwareUpdate( deviceId = "IGM401abc123", key = guestKey, ) if (info.hasUpdate) { println("Update available: ${info.firmwareUid}") println("Size: ${info.firmwareSizeBytes} bytes") } else { println("Firmware is up to date (${info.currentVersion})") } } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out reading firmware version") } catch (e: IglooWorksException) { showError("Check failed: ${e.message}") } ``` ```swift do { let info = try await sdk.checkFirmwareUpdate( deviceId: "IGM401abc123", key: guestKey) if info.hasUpdate { print("Update available: \(info.firmwareUid ?? "")") print("Size: \(info.firmwareSizeBytes ?? 0) bytes") } else { print("Firmware is up to date (\(info.currentVersion))") } } catch { showError(message: "Check failed: \(error.localizedDescription)") } ``` --- ## performDfu Performs a firmware update on the device. Returns a Flow of `DfuEvent` — progress updates then complete or failed. For BLE locks: downloads firmware, executes BLE DFU transfer with progress (0–100%), then patches the server with the new version. For SP2 devices: perform a Lock followed by an Unlock on the lock, then ensure the shackle is fully released (open position) before calling `performDfu`. For EB1 bridges: creates a cloud firmware update job and polls until completion. ### Signature ```kotlin fun performDfu( deviceId: String, key: String, firmwareUid: String, accessToken: String? = null, ): Flow ``` ```swift func performDfu( deviceId: String, key: String, firmwareUid: String ) async throws -> AsyncThrowingStream ``` There's also a public `performDfuJob(deviceId:)` for driving a Bridge's server-job DFU flow directly — `performDfu` calls into it automatically when the target device is a Bridge. ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name (or device ID for EB1). | | `key` | `String` | Yes | eKey with `ENABLE_DFU` permission. | | `firmwareUid` | `String` | Yes | Firmware package UID from `FirmwareUpdateInfo.firmwareUid`. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type — DfuEvent ```kotlin sealed class DfuEvent { /** Firmware transfer progress (0–100). */ data class Progress(val percent: Int) : DfuEvent() /** Firmware update completed. */ data class Complete(val newFirmwareVersion: String) : DfuEvent() /** Firmware update failed. */ data class Failed(val error: IglooWorksException) : DfuEvent() } ``` ```swift public enum DfuEvent { case progress(Int) case complete(newFirmwareVersion: String) } ``` ### Example ```kotlin try { val info = sdk.checkFirmwareUpdate(deviceId, key) if (!info.hasUpdate) return sdk.performDfu( deviceId = deviceId, key = key, firmwareUid = info.firmwareUid!!, ).collect { event -> when (event) { is DfuEvent.Progress -> updateProgressBar(event.percent) is DfuEvent.Complete -> showSuccess("Updated to ${event.newFirmwareVersion}") is DfuEvent.Failed -> { when (event.error) { is IglooWorksException.BatteryLowException -> showError("Battery too low for update") is IglooWorksException.DfuStateNotReadyException -> showError("Lock not ready — close the door and try again") else -> showError("DFU failed: ${event.error.message}") } } } } } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.ApiException) { showError("Server error: ${e.message}") } catch (e: IglooWorksException) { showError("Firmware update failed: ${e.message}") } ``` ```swift let info = try await sdk.checkFirmwareUpdate(deviceId: deviceId, key: key) guard info.hasUpdate, let firmwareUid = info.firmwareUid else { return } do { let events = try await sdk.performDfu(deviceId: deviceId, key: key, firmwareUid: firmwareUid) for try await event in events { switch event { case let .progress(percent): updateProgressBar(percent) case let .complete(newFirmwareVersion): showSuccess(message: "Updated to \(newFirmwareVersion)") } } } catch IglooworksError.batteryLow { showError(message: "Battery too low for update") } catch IglooworksError.dfuStateNotReady { showError(message: "Lock not ready — close the door and try again") } catch { showError(message: "Firmware update failed: \(error.localizedDescription)") } ``` ### Notes - **Battery check:** The SDK checks battery before starting DFU. Minimum 20% (31% for SP2X/SP2E). Throws `BatteryLowException` if too low. - **DFU state check:** The SDK verifies the lock is ready for DFU (door closed, not in active state). Throws `DfuStateNotReadyException` if not ready. Skipped for EK and IEF SKU. - **SP2 SKU:** Before performing DFU on an SP2 device, perform a Lock followed by an Unlock on the lock, then ensure the shackle is fully released (open position) before starting the update. - **Progress:** BLE DFU emits progress 0–90% during transfer, animates 90–99% during reconnect, then 100% after reading the new firmware version. - **Bridge DFU:** For EB1 bridges, the SDK creates a cloud job and polls every 3 seconds. `DfuJobFailedException` if the job fails, `DfuJobExpiredException` if it expires. - **Keep screen alive:** DFU can take several minutes. Keep the screen on and display a progress indicator. - Errors are emitted as `DfuEvent.Failed` rather than thrown as exceptions. ### FILE: works/sdk_getting_started Title: SDK Setup Category: BLE SDK ---------------------------------------- # SDK Setup Install the iglooworks BLE SDK and make your first lock operation. --- ## Requirements | Requirement | Version | |-------------|---------| | Android minSdk | 24 (Android 7.0) | | Android compileSdk | 35 | | Kotlin | 2.0.21+ | | Java compatibility | 1.8 | | iOS deployment target | 15.0+ | | Xcode | 16+ | | Swift | 6.0+ (Swift 6 language mode) | > BLE requires a physical iOS device — the iOS Simulator does not support Bluetooth. --- ## Installation (Android) ### 1. Add the GitLab Maven Repository In your project-level `settings.gradle.kts`: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://gitlab.com/api/v4/projects/67339313/packages/maven") credentials(HttpHeaderCredentials::class) { name = "Private-Token" value = providers.gradleProperty("iglooGitlabToken").get() } authentication { create("header") } } } } ``` Add your GitLab Personal Access Token to `gradle.properties`: ```properties iglooGitlabToken=glpat-XXXXXXXXXXXXXXXXXXXX ``` > **Security:** Do not commit `gradle.properties` with tokens to version control. Add it to `.gitignore`. ### 2. Add the SDK Dependency In your module-level `build.gradle.kts`: ```kotlin dependencies { implementation("co.igloo.works:sdk:2.3.0-build.2") } ``` --- ## Installation (iOS) ### 1. Add the Package Via Swift Package Manager, in Xcode: **File → Add Package Dependencies…**, or in `Package.swift`: ```swift dependencies: [ .package(url: "https://gitlab.com/igloohome/iglooworks-ios-sdk.git", branch: "main") ] ``` Add the `IglooworksSDK` product to your target's dependencies. > **Note:** the package repo is private — Xcode must be configured with credentials (SSH key or a GitLab personal access token) that can read `gitlab.com/igloohome/iglooworks-ios-sdk`. Set **Build Libraries for Distribution** (`BUILD_LIBRARY_FOR_DISTRIBUTION = YES`) on your app target — the SDK links a prebuilt `IglooSDKCore.xcframework`. ### 2. Import ```swift import IglooworksSDK ``` Unlike some other igloo SDKs, `IglooworksSDK` re-exports the shared Bluetooth-layer error type (`LockManagerError`) itself, so there's no need to separately `import IglooSDKCore`. --- ## Permissions ### AndroidManifest.xml ```xml ``` ### Info.plist (iOS) ```xml NSBluetoothAlwaysUsageDescription This application uses Bluetooth to connect to the Lock ``` If your app needs to keep scanning or connected while backgrounded, also add the `bluetooth-central` background mode: ```xml UIBackgroundModes bluetooth-central ``` ### Runtime Permissions Request BLE permissions before calling any SDK method: ```kotlin private val blePermissions = arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.ACCESS_FINE_LOCATION, ) private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { grants -> if (grants.values.all { it }) { // All permissions granted — safe to use SDK } } // Call before first SDK use permissionLauncher.launch(blePermissions) ``` There is no iOS equivalent of this step — just declare the `Info.plist` keys above and call SDK methods directly; the OS handles prompting. --- ## Initialization ### API Key Authentication ```kotlin val sdk = IglooPlugin(context, apiKey = "your-api-key") ``` ```swift let sdk = IglooPlugin(apiKey: "your-api-key") ``` ### OAuth Authentication ```kotlin val sdk = IglooPlugin(context) // Pass accessToken to each method that requires it ``` ```swift let sdk = IglooPlugin() // Pass accessToken to each method that requires it — an operation called // without one throws IglooworksError.api(status: 401, ...) "Missing bearer token" ``` > **Important:** Only one `IglooPlugin` instance should exist at a time. Multiple instances cause undefined BLE behavior — each holds its own connection state. --- ## Authentication Modes ```kotlin sealed class Auth { data class ApiKey(val apiKey: String) : Auth() data object OAuth : Auth() } ``` ```swift // IglooPlugin has two initializers, one per mode — no separate Auth type. public init(apiKey: String) // API Key mode public init() // OAuth mode ``` | Mode | Constructor | Token Handling | |------|------------|---------------| | API Key | `IglooPlugin(context, apiKey)` | SDK manages auth headers automatically. | | OAuth | `IglooPlugin(context)` | Pass `accessToken` parameter to methods that require server calls. | The same table on iOS: | Mode | Constructor | Token Handling | |------|------------|---------------| | API Key | `IglooPlugin(apiKey:)` | SDK manages auth headers automatically. | | OAuth | `IglooPlugin()` | Pass the relevant `...Token`/`accessToken` parameter to methods that require server calls. | When using OAuth, methods that call the server accept an `accessToken: String?` parameter. This token must be a valid OAuth bearer token with the required scopes for the operation. --- ## Quick Start ```kotlin // 1. Initialize val sdk = IglooPlugin(context, apiKey = "your-api-key") // 2. Scan for nearby locks try { sdk.scanDevice().collect { result -> println("Found: ${result.deviceId}, paired=${result.isPaired}, rssi=${result.rssi}") } } catch (e: IglooWorksException.BluetoothException) { println("Please enable Bluetooth") } catch (e: IglooWorksException) { println("Scan failed: ${e.message}") } // 3. Lock a device try { sdk.lock(deviceId = "IGM401abc123", key = guestKey) println("Locked") } catch (e: IglooWorksException.ConnectionException) { println("unexpected bluetooth connection issue") } catch (e: IglooWorksException) { println("Error: ${e.message}") } // 4. Sync device state try { val sync = sdk.sync(deviceId = "IGM401abc123", key = guestKey) println("Battery: ${sync.batteryLevel}%") } catch (e: IglooWorksException.ConnectionException) { println("unexpected bluetooth connection issue") } catch (e: IglooWorksException) { println("Sync failed: ${e.message}") } // 5. Disconnect when done sdk.disconnect("IGM401abc123") ``` ```swift // 1. Initialize let sdk = IglooPlugin(apiKey: "your-api-key") // 2. Scan for nearby locks let scanTask = Task { do { for try await result in sdk.scansLock() { print("Found: \(result.bluetoothDeviceName), paired=\(result.isPaired)") } } catch LockManagerError.bluetoothIsTurnedOff { print("Please enable Bluetooth") } catch { print("Scan failed: \(error.localizedDescription)") } } // Stop scanning once you've found what you need: sdk.stopScan() scanTask.cancel() // 3. Lock a device do { try await sdk.lock("IGM401abc123", key: guestKey) print("Locked") } catch LockManagerError.deviceDisconnected { print("unexpected bluetooth connection issue") } catch { print("Error: \(error.localizedDescription)") } // 4. Sync device state do { let result = try await sdk.sync("IGM401abc123", key: guestKey) print("Battery: \(result.batteryLevel)%") } catch LockManagerError.deviceDisconnected { print("unexpected bluetooth connection issue") } catch { print("Sync failed: \(error.localizedDescription)") } // 5. Disconnect when done try? await sdk.disconnect("IGM401abc123") ``` --- ## Next Steps - [Lock & Unlock](/works/sdk_lock_unlock) — Control locks via BLE - [Scanning](/works/sdk_scanning) — Discover nearby devices - [Pairing](/works/sdk_pairing) — Register new locks - [Error Reference](/works/sdk_errors) — All exception types and codes ### FILE: works/sdk_keycard Title: Keycard (RFID) Category: BLE SDK ---------------------------------------- # Keycard (RFID) Add and remove RFID keycards on iglooworks locks. --- ## addKeycard Opens a BLE registration window and waits for the user to tap a physical keycard on the lock's reader (45-second timeout). Once detected, registers the card on the server. If server registration fails after the card is enrolled on lock hardware, the SDK automatically removes the card from the lock. ### Signature ```kotlin suspend fun addKeycard( deviceId: String, key: String, name: String, accessToken: String? = null, ): AddKeycardResponse ``` ```swift func addKeycard( deviceId: String, key: String, name: String, accessToken token: String ) async throws -> CreateKeycardAccessResponse ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `ADD_CARD` permission. | | `name` | `String` | Yes | User-assigned name for the keycard. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type ```kotlin data class AddKeycardResponse( val accessId: String, val name: String, val payload: String, ) ``` ```swift public struct CreateKeycardAccessResponse: Codable, Sendable { public let accessId: String public let name: String public let payload: String } ``` | Field | Description | |-------|-------------| | `accessId` | Server-assigned access ID. | | `name` | The name assigned to the keycard. | | `payload` | Base64-encoded card UID. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during registration. | | `TimeoutException` | 703 | No card tapped within 45 seconds. | | `LockStorageFullException` | 912 | No storage for more keycards. | | `ApiException` | varies | Server registration failed (card rolled back from lock). | ### Example ```kotlin try { showPrompt("Tap your keycard on the lock reader...") val result = sdk.addKeycard( deviceId = "IGM401abc123", key = guestKey, name = "Office Badge", ) showSuccess("Keycard registered: ${result.accessId}") } catch (e: IglooWorksException.TimeoutException) { showError("No card detected — try again") } catch (e: IglooWorksException.LockStorageFullException) { showError("Lock is full — delete a keycard first") } catch (e: IglooWorksException) { showError("Failed: ${e.message}") } ``` ```swift do { showPrompt(message: "Tap your keycard on the lock reader...") let result = try await sdk.addKeycard( deviceId: "IGM401abc123", key: guestKey, name: "Office Badge", accessToken: accessToken) showSuccess(message: "Keycard registered: \(result.accessId)") } catch LockManagerError.LockTimeoutError { showError(message: "No card detected — try again") } catch IglooworksError.lockStorageFull { showError(message: "Lock is full — delete a keycard first") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## deleteKeycard Removes a keycard from the lock and deletes the server record. The SDK fetches the card UID from the server, removes it from lock hardware via BLE, then deletes the server record. If the card is already removed from the lock (BLE error 980), the SDK still deletes the server record. ### Signature ```kotlin suspend fun deleteKeycard( deviceId: String, key: String, accessId: String, accessToken: String? = null, ) ``` ```swift func deleteKeycard( deviceId: String, key: String, accessId: String, accessType: AccessType = .rfid, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_CARD` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the keycard to delete. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deleteKeycard( deviceId = "IGM401abc123", key = guestKey, accessId = "access-456", ) showSuccess("Keycard removed") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Delete failed: ${e.message}") } ``` ```swift do { try await sdk.deleteKeycard( deviceId: "IGM401abc123", key: guestKey, accessId: "access-456", accessToken: accessToken) showSuccess(message: "Keycard removed") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Registration timeout is 45 seconds (hardcoded). - Server deletion retries up to 3 times with 1-second delays. - `LockUidNotFoundException` (980, Android) / `IglooworksError.lockUidNotFound` (iOS) during BLE delete is non-fatal — server record is still removed. - Supported lock types: IWS, IGM3, IGM4, IGR, RG1, MP1F, ML5, RW1. ### FILE: works/sdk_linking Title: Linking Category: BLE SDK ---------------------------------------- # Linking Link and unlink accessories (Keypads, Key Fobs, Bridges) to locks. --- ## link Links an accessory to a lock. The SDK orchestrates server registration, cipher resolution, and BLE link command. If the BLE step fails, the server link is automatically rolled back. ### Signature ```kotlin suspend fun link( accessoryDeviceId: String, accessoryGuestKey: String, lockDeviceId: String, accessToken: String? = null, ): LinkResult ``` ```swift func link( accessoryDeviceId: String, accessoryGuestKey: String, lockBluetoothDeviceName: String, token: String? = nil ) async throws -> LinkResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `accessoryDeviceId` | `String` | Yes | Bluetooth device name of the accessory (e.g. `"EK1X01abc123"`). | | `accessoryGuestKey` | `String` | Yes | BLE key for the accessory. Keypad: eKey with `ADD_LOCK` permission. Fob: admin key. | | `lockDeviceId` | `String` | Yes | Bluetooth device name of the lock to link to. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type ```kotlin data class LinkResult( val accessoryId: String, val lockId: String, val linkedAt: String, ) ``` ```swift public struct LinkResult { public let accessoryId: String public let lockId: String public let linkedAt: String } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during linking. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `BridgeOfflineException` | 406 | Bridge is offline (bridge link path). | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { val result = sdk.link( accessoryDeviceId = "EK1X01abc123", accessoryGuestKey = ekeyWithAddLock, lockDeviceId = "IGM401abc123", ) showSuccess("Linked ${result.accessoryId} to ${result.lockId}") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Link failed: ${e.message}") } ``` ```swift do { let result = try await sdk.link( accessoryDeviceId: "EK1X01abc123", accessoryGuestKey: ekeyWithAddLock, lockBluetoothDeviceName: "IGM401abc123") showSuccess(message: "Linked \(result.accessoryId) to \(result.lockId)") } catch { showError(message: "Link failed: \(error.localizedDescription)") } ``` --- ## unlink Unlinks an accessory from a lock. The SDK removes the link via BLE, then deletes the server record. BLE errors 890 (`PinNotFoundException`) and 980 (`LockUidNotFoundException`) are treated as "already unlinked on the accessory side" — the SDK continues to remove the server link. ### Signature ```kotlin suspend fun unlink( accessoryDeviceId: String, accessoryGuestKey: String, lockDeviceId: String, accessToken: String? = null, ) ``` ```swift func unlink( accessoryDeviceId: String, accessoryGuestKey: String, lockBluetoothDeviceName: String, token: String? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `accessoryDeviceId` | `String` | Yes | Bluetooth device name of the accessory. | | `accessoryGuestKey` | `String` | Yes | BLE key for the accessory. Keypad: eKey with `DELETE_LOCK` permission. Fob: admin key. | | `lockDeviceId` | `String` | Yes | Bluetooth device name of the lock to unlink from. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during unlinking. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `BridgeOfflineException` | 406 | Bridge is offline (bridge unlink path). | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { sdk.unlink( accessoryDeviceId = "EK1X01abc123", accessoryGuestKey = ekeyWithDeleteLock, lockDeviceId = "IGM401abc123", ) showSuccess("Accessory unlinked") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Unlink failed: ${e.message}") } ``` ```swift do { try await sdk.unlink( accessoryDeviceId: "EK1X01abc123", accessoryGuestKey: ekeyWithDeleteLock, lockBluetoothDeviceName: "IGM401abc123") showSuccess(message: "Accessory unlinked") } catch { showError(message: "Unlink failed: \(error.localizedDescription)") } ``` ### Notes - Always unlink accessories before unpairing the lock — `unpair()` will throw `HasLinkedDeviceException` (Android) / `IglooworksError.hasLinkedDevice` (iOS) if linked devices exist. - The accessory key type depends on the accessory type: - **Keypad (EK1, EK2):** Generate an eKey with appropriate permission via the eKey API. - **Key Fob (IEF):** Use the admin key from `GET /devices/{device_id}/admin-key`. - Bridge accessories (`"EB1"` prefix) use a server-job link/unlink flow instead of a BLE flow. `link`/`unlink` work for Bridges, throwing `BridgeOfflineException` if the Bridge is offline. ### FILE: works/sdk_lock_unlock Title: Lock & Unlock Category: BLE SDK ---------------------------------------- # Lock & Unlock Control iglooworks smart locks via BLE. --- ## lock Locks the device via Bluetooth. Optionally sets the lock's internal clock before locking. ### Signature ```kotlin suspend fun lock( deviceId: String, key: String, timeInSeconds: Long? = null, operationId: Int? = null, ) ``` ```swift func lock( _ deviceId: String, key: String, operationId: Int? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock (e.g. `"IGM401abc123"`). | | `key` | `String` | Yes | eKey for BLE authentication. Required permission: `LOCK`. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock before locking. If omitted, the SDK uses the last server time if available. | | `operationId` | `Int?` | No | Operation ID for tracking the BLE command in activity logs. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected. | | `TimeoutException` | 703 | Operation exceeded the 15-second BLE timeout. | | `GenericException` | 1 | Any other unhandled error. | On iOS, `lock`/`unlock` throw `LockManagerError` directly — see [Error Reference](/works/sdk_errors). ### Example ```kotlin try { sdk.lock( deviceId = "IGM401abc123", key = guestKey, ) showSuccess("Door locked") } catch (e: IglooWorksException) { when (e) { is IglooWorksException.BluetoothException -> showError("Please enable Bluetooth") is IglooWorksException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooWorksException.TimeoutException -> showError("Operation timed out, try again") else -> showError("Failed: ${e.message}") } } ``` ```swift do { try await sdk.lock("IGM401abc123", key: guestKey) showSuccess(message: "Door locked") } catch LockManagerError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch LockManagerError.LockTimeoutError { showError(message: "Operation timed out, try again") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## unlock Unlocks the device via Bluetooth. Optionally sets the lock's internal clock before unlocking. ### Signature ```kotlin suspend fun unlock( deviceId: String, key: String, timeInSeconds: Long? = null, operationId: Int? = null, ) ``` ```swift func unlock( _ deviceId: String, key: String, operationId: Int? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey for BLE authentication. Required permission: `UNLOCK`. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock before unlocking. | | `operationId` | `Int?` | No | Operation ID for tracking the BLE command in activity logs. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected. | | `TimeoutException` | 703 | Operation exceeded the 15-second BLE timeout. | | `GenericException` | 1 | Any other unhandled error. | ### Example ```kotlin try { sdk.unlock( deviceId = "IGM401abc123", key = guestKey, ) showSuccess("Door unlocked") } catch (e: IglooWorksException) { when (e) { is IglooWorksException.ConnectionException -> showError("unexpected bluetooth connection issue") is IglooWorksException.TimeoutException -> showError("Timed out, try again") else -> showError("Failed: ${e.message}") } } ``` ```swift do { try await sdk.unlock("IGM401abc123", key: guestKey) showSuccess(message: "Door unlocked") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch LockManagerError.LockTimeoutError { showError(message: "Timed out, try again") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` --- ## disconnect Closes the BLE connection to a specific lock. Safe to call even if not connected. ### Signature ```kotlin suspend fun disconnect(deviceId: String) ``` ```swift func disconnect(_ deviceId: String) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock to disconnect from. | ### Example ```kotlin sdk.disconnect("IGM401abc123") ``` ```swift try? await sdk.disconnect("IGM401abc123") ``` --- ## disconnectAll Closes all active BLE connections. ### Signature ```kotlin suspend fun disconnectAll() ``` ### Example ```kotlin sdk.disconnectAll() ``` ### Notes - The default BLE timeout is 15 seconds for lock/unlock operations. - The SDK automatically connects to the lock when `lock()` or `unlock()` is called — there is no separate `connect()` method. - Always call `disconnect()` or `disconnectAll()` when done to free BLE resources. On iOS, call `disconnect(_:)` for each device you connected to. - If the lock disconnects during an operation, `ConnectionException` (Android) / `LockManagerError.deviceDisconnected` (iOS) is thrown immediately. ### FILE: works/sdk_pairing Title: Pairing Category: BLE SDK ---------------------------------------- # Pairing Register and remove iglooworks devices from your account. --- ## pair Pairs an Igloo lock via Bluetooth and registers it on the server. Protocol (G2/G3) is auto-detected from the lock's firmware. The SDK orchestrates the full flow: timezone resolution, DST data fetch, BLE handshake, server registration, and commit. If commit fails after server registration, the device is automatically rolled back (deleted from server). ### Signature ```kotlin suspend fun pair( deviceId: String, name: String, propertyIds: List, accessToken: String? = null, ): PairResult ``` ```swift func pair( deviceId: String, lockName: String, propertyIds: [String], pairAccessToken token: String? = nil ) async throws -> PairResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock (e.g. `"SP2X01abc123"`). | | `name` | `String` | Yes | User-assigned name for the lock. | | `propertyIds` | `List` | Yes | IDs of properties to assign this lock to. Must be non-empty. All properties must share the same timezone. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Return Type ```kotlin data class PairResult( val type: String, val deviceId: String, val name: String, val pairedAt: String, val properties: List, ) data class Property( val name: String, val timezone: String, val id: String, ) ``` ```swift public struct PairResult: Sendable { public let type: String public let bluetoothDeviceName: String public let name: String public let pairedAt: String public let properties: [Property] } public struct Property: Sendable { public let name: String public let timezone: String public let id: String } ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | BLE connection failed or device disconnected during pairing. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `DevicePairedException` | 910 | Device is already paired (detected server-side or via BLE scan). | | `ApiException` | varies | Server API call failed. | | `GenericException` | 1 | Property IDs empty, timezone conflict, or unsupported protocol. | ### Example ```kotlin try { val result = sdk.pair( deviceId = "SP2X01abc123", name = "Front Door Lock", propertyIds = listOf("prop-123"), ) println("Paired: ${result.name} at ${result.pairedAt}") println("Properties: ${result.properties.map { it.name }}") } catch (e: IglooWorksException.DevicePairedException) { showError("This lock is already paired") } catch (e: IglooWorksException.HasLinkedDeviceException) { showError("Unlink accessories first: ${e.linkedDeviceIds}") } catch (e: IglooWorksException) { showError("Pairing failed: ${e.message}") } ``` ```swift do { let result = try await sdk.pair( deviceId: "SP2X01abc123", lockName: "Front Door Lock", propertyIds: ["prop-123"]) print("Paired: \(result.name) at \(result.pairedAt)") print("Properties: \(result.properties.map(\.name))") } catch IglooworksError.devicePaired { showError(message: "This lock is already paired") } catch IglooworksError.hasLinkedDevice(let linkedDeviceIds) { showError(message: "Unlink accessories first: \(linkedDeviceIds)") } catch { showError(message: "Pairing failed: \(error.localizedDescription)") } ``` ### Notes - The device must be in pairing mode (unpaired state). - All `propertyIds` must share the same timezone — the SDK fetches DST data based on this shared timezone. - G3 locks (protocol version 2) use certificate-based pairing with an additional server round-trip. - If the BLE commit step fails after server registration, the SDK automatically deletes the server record. --- ## unpair Unpairs a lock via Bluetooth and removes it from the server. The SDK checks for linked devices first. If the lock has linked accessories (keypads, fobs, bridges), it throws `HasLinkedDeviceException` — you must unlink them first. ### Signature ```kotlin suspend fun unpair( deviceId: String, accessToken: String? = null, ) ``` ```swift func unpair( deviceId: String, accessToken token: String? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during unpair. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `HasLinkedDeviceException` | 910 | Device has linked accessories. Contains `linkedDeviceIds: List`. | | `ApiException` | varies | Server API call failed. | ### Example ```kotlin try { sdk.unpair(deviceId = "SP2X01abc123") showSuccess("Lock removed") } catch (e: IglooWorksException.HasLinkedDeviceException) { showError("Unlink these devices first: ${e.linkedDeviceIds}") } catch (e: IglooWorksException) { showError("Unpair failed: ${e.message}") } ``` ```swift do { try await sdk.unpair(deviceId: "SP2X01abc123") showSuccess(message: "Lock removed") } catch IglooworksError.hasLinkedDevice(let linkedDeviceIds) { showError(message: "Unlink these devices first: \(linkedDeviceIds)") } catch { showError(message: "Unpair failed: \(error.localizedDescription)") } ``` ### Notes - The SDK automatically fetches the correct key for BLE unpair (eKey for G3, admin key for G2). `unpair` on both platforms does not take a `key` parameter — it is resolved internally. - Server deletion retries up to 3 times with 5-second delays on failure. - After unpair, all access credentials (PINs, keycards, fingerprints) are removed. --- ## setWifiConfig Configures WiFi on an Igloo Bridge device via Bluetooth. After sending WiFi credentials via BLE, the SDK generates a CSR on the bridge and polls the server until the bridge is provisioned and online. ### Signature ```kotlin suspend fun setWifiConfig( deviceId: String, key: String, ssid: String, networkPassword: String, accessToken: String? = null, ) ``` ```swift func setWifiConfig( deviceId: String, key: String, ssid: String, networkPassword: String, accessToken token: String? = nil ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the bridge (e.g. `"EB1X01abc123"`). | | `key` | `String` | Yes | Admin key for BLE authentication. | | `ssid` | `String` | Yes | WiFi network SSID. | | `networkPassword` | `String` | Yes | WiFi network password. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during setup. | | `TimeoutException` | 703 | Bridge was not provisioned or online after all polling retries (15s, 30s, 45s). | ### Example ```kotlin try { sdk.setWifiConfig( deviceId = "EB1X01abc123", key = adminKey, ssid = "Office-WiFi", networkPassword = "password123", ) showSuccess("Bridge WiFi configured and online") } catch (e: IglooWorksException.TimeoutException) { showError("Bridge did not come online — check WiFi credentials") } catch (e: IglooWorksException) { showError("WiFi setup failed: ${e.message}") } ``` ```swift do { try await sdk.setWifiConfig( deviceId: "EB1X01abc123", key: adminKey, ssid: "Office-WiFi", networkPassword: "password123") showSuccess(message: "Bridge WiFi configured and online") } catch IglooworksError.timeout { showError(message: "Bridge did not come online — check WiFi credentials") } catch { showError(message: "WiFi setup failed: \(error.localizedDescription)") } ``` ### Notes - The SDK sends WiFi credentials + MQTT broker config to the bridge in a single BLE command. - After the BLE write, the SDK generates a private key CSR on the bridge. - The SDK then polls the server at 15s, 30s, and 45s intervals until `isProvisioned` and `isOnline` are both `true`. - If the bridge doesn't come online after all retries, `TimeoutException` (Android) / `IglooworksError.timeout` (iOS) is thrown. ### FILE: works/sdk_pin Title: PIN Management Category: BLE SDK ---------------------------------------- # PIN Management Create and delete custom PINs on iglooworks locks. --- ## createPin Creates a custom PIN on the lock via Bluetooth and registers it on the server. The SDK writes the PIN to lock hardware first, then registers on the server. If server registration fails, the SDK automatically deletes the PIN from the lock. ### Signature ```kotlin suspend fun createPin( deviceId: String, key: String, pin: String, pinType: PinType, name: String, startTimeInSeconds: Long? = null, endTimeInSeconds: Long? = null, accessToken: String? = null, ): CreatePinResponse ``` ```swift func createPin( deviceId: String, key: String, name: String, pin: String, pinType: PinType, startDate: Date?, endDate: Date?, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `CREATE_PIN` permission. | | `pin` | `String` | Yes | PIN code (numeric, 1–12 digits). | | `pinType` | `PinType` | Yes | PIN type: `ONETIME`, `PERMANENT`, or `DURATION`. | | `name` | `String` | Yes | User-assigned name for the PIN. | | `startTimeInSeconds` | `Long?` | Duration only | Start time in epoch seconds. Required for `DURATION` type. | | `endTimeInSeconds` | `Long?` | Duration only | End time in epoch seconds. Required for `DURATION` type. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Supporting Types ```kotlin enum class PinType { ONETIME, PERMANENT, DURATION, } ``` ```swift public enum PinType: String, Sendable { case otp = "otp" case permanent = "permanent" case duration = "duration" } ``` ### Return Type ```kotlin data class CreatePinResponse( val accessId: String, val name: String, val pin: String, val pinType: String, val startDateTime: String?, val endDateTime: String?, ) ``` ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during PIN creation. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `DuplicatePinException` | 880 | PIN already exists on the lock. | | `LockStorageFullException` | 912 | No storage for more PINs on the lock. | | `ApiException` | varies | Server registration failed (PIN rolled back from lock). | ### Example ```kotlin try { val result = sdk.createPin( deviceId = "IGM401abc123", key = guestKey, pin = "123456", pinType = PinType.PERMANENT, name = "Front Door PIN", ) println("PIN created: ${result.accessId}") } catch (e: IglooWorksException.DuplicatePinException) { showError("This PIN already exists on the lock") } catch (e: IglooWorksException.LockStorageFullException) { showError("Lock is full — delete a PIN first") } catch (e: IglooWorksException) { showError("Failed: ${e.message}") } ``` ```swift do { try await sdk.createPin( deviceId: "IGM401abc123", key: guestKey, name: "Front Door PIN", pin: "123456", pinType: .permanent, startDate: nil, endDate: nil, accessToken: accessToken) showSuccess(message: "PIN created") } catch IglooworksError.duplicatePin { showError(message: "This PIN already exists on the lock") } catch IglooworksError.lockStorageFull { showError(message: "Lock is full — delete a PIN first") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - On iOS, passing `pinType: .duration` without both `startDate` and `endDate` throws `IglooworksError.genericError` immediately, before any Bluetooth command is sent. --- ## deletePin Deletes a PIN from the lock and removes the server record. The SDK fetches the PIN value from the server, removes it from lock hardware via BLE, then deletes the server record. If the PIN is already removed from the lock (BLE error 890), the SDK still deletes the server record. ### Signature ```kotlin suspend fun deletePin( deviceId: String, key: String, accessId: String, accessToken: String? = null, ) ``` ```swift func deletePin( deviceId: String, key: String, accessId: String, accessToken token: String ) async throws ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `DELETE_PIN` permission. | | `accessId` | `String` | Yes | Server-assigned access ID of the PIN to delete. | | `accessToken` | `String?` | OAuth only | OAuth access token for server calls. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during deletion. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Server deletion failed. | ### Example ```kotlin try { sdk.deletePin( deviceId = "IGM401abc123", key = guestKey, accessId = "access-123", ) showSuccess("PIN deleted") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Delete failed: ${e.message}") } ``` ```swift do { try await sdk.deletePin( deviceId: "IGM401abc123", key: guestKey, accessId: "access-123", accessToken: accessToken) showSuccess(message: "PIN deleted") } catch { showError(message: "Failed: \(error.localizedDescription)") } ``` ### Notes - Server deletion retries up to 3 times with 1-second delays on failure. - `PinNotFoundException` (890, Android) / `IglooworksError.pinNotFound` (iOS) during BLE delete is treated as non-fatal — the SDK continues to delete the server record. ### FILE: works/sdk_scanning Title: Scanning Category: BLE SDK ---------------------------------------- # Scanning Discover nearby iglooworks smart locks via Bluetooth. --- ## scanDevice Scans for nearby Igloo locks via Bluetooth. Emits a `ScanResult` for each discovered device. The scan runs for as long as the Flow has active collectors. ### Signature ```kotlin fun scanDevice(): Flow ``` ```swift func scansLock() -> AsyncThrowingStream func stopScan() ``` ### Return Type ```kotlin data class ScanResult( val deviceId: String, val isPaired: Boolean, val isActive: Boolean, val rssi: Int, ) ``` ```swift public struct ScanResult: Identifiable, Hashable, Sendable { public var id: String { bluetoothDeviceName } public let bluetoothDeviceName: String public let isActive: Bool public let isPaired: Bool public let isG3: Bool } ``` | Field | Description | |-------|-------------| | `deviceId` | Bluetooth device name (e.g. `"IGM401abc123"`, `"SP2X01abc123"`). | | `isPaired` | `true` if the device is already paired with an account. | | `isActive` | `true` if the device is actively broadcasting. | | `rssi` | Signal strength in dBm. Higher (closer to 0) means closer. | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | ### Example ```kotlin val scanJob = viewModelScope.launch { try { sdk.scanDevice().collect { result -> println("Found: ${result.deviceId}") println(" paired=${result.isPaired}, active=${result.isActive}, rssi=${result.rssi}") } } catch (e: IglooWorksException.BluetoothException) { showError("Please enable Bluetooth") } catch (e: IglooWorksException) { showError("Scan failed: ${e.message}") } } // Stop scanning scanJob.cancel() ``` ```swift let scanTask = Task { do { for try await result in sdk.scansLock() { print("Found: \(result.bluetoothDeviceName)") print(" paired=\(result.isPaired), active=\(result.isActive), type=\(result.type)") } } catch LockManagerError.bluetoothIsTurnedOff { showError(message: "Please enable Bluetooth") } catch { showError(message: "Scan failed: \(error.localizedDescription)") } } // Stop scanning sdk.stopScan() scanTask.cancel() ``` ### Notes - The scan runs indefinitely until the collecting coroutine is cancelled (Android) or `stopScan()` is called and the enclosing `Task` is cancelled (iOS). - Ensure BLE permissions are granted before calling `scanDevice()` / `scansLock()`. On iOS this means `NSBluetoothAlwaysUsageDescription` is set in `Info.plist` — there is no runtime permission prompt to request explicitly. - The `deviceId` / `bluetoothDeviceName` prefix indicates the lock type (e.g. `IGM4` = mortise lock, `SP2X` = smart padlock, `EB1` = bridge). On iOS this is also exposed structurally via `ScanResult.type: LockType`. - Unpaired devices (`isPaired = false`) can be registered via `pair()`. - Filter by `rssi` to find nearby devices — typical threshold is `-70` for "close enough to connect". Not available on iOS (no `rssi` field). ### FILE: works/sdk_sync Title: Sync Category: BLE SDK ---------------------------------------- # Sync Synchronize device state and execute pending jobs. --- ## sync Syncs the lock via Bluetooth: sets the lock's internal clock, retrieves battery level, and uploads activity logs to the server. ### Signature ```kotlin suspend fun sync( deviceId: String, key: String, timeInSeconds: Long? = null, accessToken: String? = null, operationId: Int? = null, ): SyncResult ``` ```swift func sync( _ deviceId: String, key: String, timeInSeconds time: Int? = nil, syncToken token: String? = nil ) async throws -> SyncResult ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `GET_TIME`, `SET_TIME`, `GET_BATTERY_LEVEL`, `GET_LOGS` permissions. | | `timeInSeconds` | `Long?` | No | Unix timestamp (seconds) to set on the lock. If omitted, SDK uses last server time. | | `accessToken` | `String?` | OAuth only | OAuth access token for uploading activity logs. | | `operationId` | `Int?` | No | Operation ID for tracking. | ### Return Type ```kotlin data class SyncResult(val batteryLevel: Int) ``` ```swift public struct SyncResult: Sendable { public let batteryLevel: Int } ``` | Field | Description | |-------|-------------| | `batteryLevel` | Battery level as percentage (0–100). | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `BluetoothException` | 708 | Bluetooth is off or unavailable. | | `ConnectionException` | 12 | Device disconnected during sync. | | `TimeoutException` | 703 | BLE operation exceeded the timeout. | | `ApiException` | varies | Activity log upload failed. | ### Example ```kotlin try { val result = sdk.sync( deviceId = "IGM401abc123", key = guestKey, ) println("Battery: ${result.batteryLevel}%") } catch (e: IglooWorksException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooWorksException.TimeoutException) { showError("Timed out, try again") } catch (e: IglooWorksException) { showError("Sync failed: ${e.message}") } ``` ```swift do { let result = try await sdk.sync( "IGM401abc123", key: guestKey) print("Battery: \(result.batteryLevel)%") } catch LockManagerError.deviceDisconnected { showError(message: "unexpected bluetooth connection issue") } catch { showError(message: "Sync failed: \(error.localizedDescription)") } ``` ### Notes - Sync reads all pending activity logs from the lock in a loop until no more remain. - Logs are stored in a local database first, then batch-uploaded to the server. - If `timeInSeconds` is omitted and no cached server time is available, the lock time is not updated. --- ## syncJob Executes pending jobs (created via the API or iglooworks Dashboard) on the lock. Results are emitted as a Flow — one `SyncJobResult` per job. Supported job types: `CREATE_PIN`, `DELETE_PIN`, `ADD_GROUP_KEY`, `DELETE_GROUP_KEY`. ### Signature ```kotlin fun syncJob( deviceId: String, key: String, jobIds: List? = null, accessToken: String?, ): Flow ``` ```swift func syncJobs( _ jobIds: [String] = [], from deviceId: String, key: String, syncJobToken token: String? = nil ) async -> AsyncThrowingStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | eKey with `CREATE_PIN`, `DELETE_PIN`, `ADD_GROUP_KEY`, `DELETE_GROUP_KEY` permissions. | | `jobIds` | `List?` | No | Specific job IDs to execute. If null, all pending jobs are fetched and executed. | | `accessToken` | `String?` | OAuth only | OAuth access token for API calls. | ### Return Type ```kotlin data class SyncJobResult( val jobId: String, val jobType: String, val status: JobStatus, val remainingJobs: Int, val reason: IglooWorksException? = null, ) ``` ```swift public struct SyncJobResult: Codable, Sendable { public let jobId: String public let jobType: String public let status: SyncJobStatus public let remainingJobs: Int public var reason: String? = nil } public enum SyncJobStatus: String, Codable, Sendable { case success = "success" case failed = "failed" case pending = "pending" } ``` | Field | Description | |-------|-------------| | `jobId` | Server-assigned job identifier. | | `jobType` | Job description string (e.g. `"CREATE_PIN"`). | | `status` | `COMPLETE`, `FAILED`, or `PENDING`. | | `remainingJobs` | Number of jobs remaining after this one. | | `reason` | Exception if the job failed or was left pending (e.g. timeout). | ### Error Codes | Exception | Code | Description | |-----------|------|-------------| | `ApiException` | varies | API call failed when fetching or updating job status. | | `GenericException` | 1 | Unhandled error. | ### Example ```kotlin try { sdk.syncJob( deviceId = "IGM401abc123", key = guestKey, accessToken = token, ).collect { result -> println("Job ${result.jobId}: ${result.status} (${result.remainingJobs} remaining)") if (result.status == JobStatus.FAILED) { println(" Reason: ${result.reason?.message}") } } } catch (e: IglooWorksException.ApiException) { showError("Server error: ${e.message}") } catch (e: IglooWorksException) { showError("Sync jobs failed: ${e.message}") } ``` ```swift for try await result in await sdk.syncJobs( from: "IGM401abc123", key: guestKey, syncJobToken: token ) { print("Job \(result.jobId): \(result.status.rawValue) (\(result.remainingJobs) remaining)") if result.status == .failed { print(" Reason: \(result.reason ?? "unknown")") } } ``` ### Notes - Jobs already marked `COMPLETE` or `FAILED` on the server are emitted as-is without re-execution. - If a BLE operation fails with `TimeoutException`, `ConnectionException`, or `BluetoothException`, the job is left as `PENDING` (not marked failed on the server). - `DuplicatePinException` during `CREATE_PIN` is treated as success (job marked `COMPLETE`). - `PinNotFoundException` during `DELETE_PIN` is treated as success (PIN already removed). - On iOS the method is named `syncJobs` (plural), takes job IDs as the first positional parameter (`_ jobIds: [String] = []`, empty means "all pending"), and `reason` on `SyncJobResult` is a plain `String?`, not an `Error`/exception type. ## Bridge ### FILE: home/bridge-overview Title: Bridge Overview Category: Bridge ---------------------------------------- # Bridge Overview A Bridge puts your locks online. Your application sends a job to the igloohome API, the API forwards it to the Bridge over the internet, and the Bridge runs it on the lock over Bluetooth. Results come back to you as webhooks. Without a Bridge, you can only reach a lock from a phone standing next to it, through the igloohome app or the [Mobile SDK](/home/sdk_getting_started). ## How a Job Flows ```mermaid sequenceDiagram participant App as Your Application participant API as igloohome API participant Bridge participant Lock rect rgb(255, 245, 243) Note over App,API: 1. Create the job App->>API: POST /devices/{deviceId}/jobs/bridges/{bridgeId} API-->>App: jobId end rect rgb(248, 248, 248) Note over API,Lock: 2. Run the job API->>Bridge: Job over WiFi or Ethernet Bridge->>Lock: Command over Bluetooth Lock-->>Bridge: Result end rect rgb(255, 245, 243) Note over App,Bridge: 3. Get the result Bridge-->>API: Result API-->>App: Job Complete webhook (jobId, jobStatus) end ``` The `POST` returns a `jobId` as soon as the API accepts the job. The lock has not run the command yet. Treat the job as done only when the Job Complete webhook for that `jobId` arrives. ## Lock Jobs and Bridge Jobs | | Lock jobs | Bridge jobs | | --- | --- | --- | | Target | A lock, through its Bridge | The Bridge itself | | Endpoint | `POST /devices/{deviceId}/jobs/bridges/{bridgeId}` | `POST /devices/{bridgeId}/jobs` | | Examples | Lock, unlock, create or delete a PIN, get battery level, get activity logs | Get the Bridge's device status | | Guide | [Lock Jobs](/home/api_jobs#lock-jobs) | [Bridge Jobs](/home/api_jobs#bridge-jobs) | ## Before You Start - **Set up the Bridge.** Connect it to the internet in the igloohome app. - **Link each lock to a Bridge.** Use the igloohome app, or the [Mobile SDK](/home/sdk_linking) for in-app linking. The API cannot link locks. - **Register a webhook URL.** See [Webhook Security](/home/webhook-security#setting-up-your-webhook-url). - **Request the right scopes.** Each job type has its own scope. See [Available Scopes](/home/individual-account#available-scopes). ## Supported Bridges | Prefix | Bridge | Connectivity | | --- | --- | --- | | EB1X | Bridge | 2.4 GHz WiFi | | EB2X | Bridge 2 | 2.4 GHz WiFi, or Ethernet with the Ethernet adapter accessory | Each Bridge serves up to **5 locks**. Bluetooth range depends on the lock model, so test the Bridge position with each lock you install. ## Job Lifecycle | Stage | What you see | | --- | --- | | Accepted | `POST` returns `200` with a `jobId` | | Rejected | `POST` returns an error, for example `406` when the Bridge is offline or the lock is not linked | | Completed | Job Complete webhook with `jobStatus` `0` (success) | | Failed | Job Complete webhook with a non-zero `jobStatus`, for example when the lock is out of range | | Timed out | No webhook. Poll the job status | Most jobs complete in under a minute. A job with no result after **2 minutes** times out. A timed-out job sends no webhook, so poll the job status to see it. | `jobStatus` | Meaning | | --- | --- | | `0` | Success | | `1` | Generic error | | `2` | In progress: the Bridge picked up the job and the lock is running it | ## Getting Results Use webhooks. The Job Complete webhook carries the result and any data the job returned, such as the battery level. See [Bridge Events](/home/bridge-events) and [Job Result Payloads](/home/job-result-payloads). If a webhook does not arrive, poll [Get Job Status](/home/api_jobs#get-job-status) as a fallback. Poll every 10 seconds, and no more often than every 5 seconds. ## Events Without a Job The Bridge also sends webhooks that no job triggered: - **Activity Log Received**: lock events the Bridge picks up from locks in range, such as unlocks and PIN entries. The Bridge sends them as close to real time as it can, on a best-effort basis. - **Bridge Connection**: the Bridge went online or offline. See [Bridge Events](/home/bridge-events) for payloads. ## Ordering and Retries - Send at most **5 jobs per Bridge** in any 2-minute window. Above that, jobs are more likely to fail. - The API does not guarantee job order. If order matters, wait for one job's result before you send the next. - The API does not retry failed jobs. Create a new job to try again. - `jobStatus` `2` means the job is still running. Before you send it again, wait for the result or the 2-minute timeout, or the lock may run the command twice. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | `406` `Unable to contact bridge as it appears to be offline` | Bridge has no power or internet | Check power and the network. Watch for the Bridge Connection webhook | | `406` `Lock is not linked to bridge` | Lock is not linked to this Bridge | Link the lock in the igloohome app | | Job Complete with `jobStatus` `1` | Lock out of range, or the command failed on the lock | Move the Bridge closer to the lock, then send a new job | | No webhook after 2 minutes | Job timed out, or your webhook URL is missing or unreachable | Check your webhook URL, then poll [Get Job Status](/home/api_jobs#get-job-status) | | `429` on a Bridge job | More than one Get Device Status job per Bridge in 5 minutes | Wait for the `Retry-After` seconds | ## Next Steps - [Lock Jobs](/home/api_jobs#lock-jobs): send commands to a lock. - [Bridge Jobs](/home/api_jobs#bridge-jobs): check the Bridge itself. - [Bridge Events](/home/bridge-events): handle the webhooks. ### FILE: works/bridge-overview Title: Bridge Overview Category: Bridge ---------------------------------------- # Bridge Overview A Bridge puts your locks online. Your application sends a job to the iglooworks API, the API forwards it to the Bridge over the internet, and the Bridge runs it on the lock over Bluetooth. Results come back to you as webhooks. Without a Bridge, you can only reach a lock from a phone standing next to it, through the iglooworks app or the [Mobile SDK](/works/sdk_getting_started). ## How a Job Flows ```mermaid sequenceDiagram participant App as Your Application participant API as iglooworks API participant Bridge participant Lock rect rgb(255, 245, 243) Note over App,API: 1. Create the job App->>API: POST /devices/{deviceId}/jobs/bridges/{bridgeId} API-->>App: jobId end rect rgb(248, 248, 248) Note over API,Lock: 2. Run the job API->>Bridge: Job over WiFi or Ethernet Bridge->>Lock: Command over Bluetooth Lock-->>Bridge: Result end rect rgb(255, 245, 243) Note over App,Bridge: 3. Get the result Bridge-->>API: Result API-->>App: Job Complete webhook (jobId, jobStatus) end ``` The `POST` returns a `jobId` as soon as the API accepts the job. The lock has not run the command yet. Treat the job as done only when the Job Complete webhook for that `jobId` arrives. ## Lock Jobs Every Bridge job in the iglooworks API targets a lock through its Bridge: lock, unlock, create or delete a PIN, get battery level, get device status, or get activity logs. Pass `departmentId` unless the account has a single department. See [Lock Jobs](/works/api_jobs#lock-jobs). ## Before You Start - **Set up the Bridge.** Connect it to the internet in the iglooworks app. - **Link each lock to a Bridge.** Use the iglooworks app, or the [Mobile SDK](/works/sdk_linking) for in-app linking. The API cannot link locks. - **Register a webhook URL.** See [Webhook Security](/works/webhook-security#setting-up-your-webhook-url). - **Request the right scopes.** Each job type has its own scope. See [Available Scopes](/works/individual-account#available-scopes). ## Supported Bridges | Prefix | Bridge | Connectivity | | --- | --- | --- | | EB1X | Bridge | 2.4 GHz WiFi | | EB2X | Bridge 2 | 2.4 GHz WiFi, or Ethernet with the Ethernet adapter accessory | Each Bridge serves up to **10 locks**. Bluetooth range depends on the lock model, so test the Bridge position with each lock you install. ## Job Lifecycle | Stage | What you see | | --- | --- | | Accepted | `POST` returns `200` with a `jobId` | | Rejected | `POST` returns an error, for example `406` when the Bridge is offline or the lock is not linked | | Completed | Job Complete webhook with `jobStatus` `0` (success) | | Failed | Job Complete webhook with a non-zero `jobStatus`, for example when the lock is out of range | | Timed out | No webhook. Poll the job status | Most jobs complete in under a minute. A job with no result after **2 minutes** times out. A timed-out job sends no webhook, so poll the job status to see it. | `jobStatus` | Meaning | | --- | --- | | `0` | Success | | `1` | Generic error | | `2` | In progress: the Bridge picked up the job and the lock is running it | ## Getting Results Use webhooks. The Job Complete webhook carries the result and any data the job returned, such as the battery level. See [Bridge Events](/works/bridge-events) and [Job Result Payloads](/works/job-result-payloads). If a webhook does not arrive, poll [Get Bridge Job Status](/works/api_jobs#get-bridge-job-status) as a fallback. Poll every 10 seconds, and no more often than every 5 seconds. ## Events Without a Job The Bridge also sends webhooks that no job triggered: - **Activity Log Received**: lock events the Bridge picks up from locks in range, such as unlocks and PIN entries. The Bridge sends them as close to real time as it can, on a best-effort basis. - **Bridge Connection**: the Bridge went online or offline. See [Bridge Events](/works/bridge-events) for payloads. ## Ordering and Retries - Send at most **5 jobs per Bridge** in any 2-minute window. Above that, jobs are more likely to fail. - The API does not guarantee job order. If order matters, wait for one job's result before you send the next. - The API does not retry failed jobs. Create a new job to try again. - `jobStatus` `2` means the job is still running. Before you send it again, wait for the result or the 2-minute timeout, or the lock may run the command twice. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | `406` `Unable to contact bridge as it appears to be offline` | Bridge has no power or internet | Check power and the network. Watch for the Bridge Connection webhook | | `406` `Lock is not linked to bridge` | Lock is not linked to this Bridge | Link the lock in the iglooworks app | | Job Complete with `jobStatus` `1` | Lock out of range, or the command failed on the lock | Move the Bridge closer to the lock, then send a new job | | No webhook after 2 minutes | Job timed out, or your webhook URL is missing or unreachable | Check your webhook URL, then poll [Get Bridge Job Status](/works/api_jobs#get-bridge-job-status) | ## Next Steps - [Lock Jobs](/works/api_jobs#lock-jobs): send commands to a lock. - [Bridge Events](/works/bridge-events): handle the webhooks. ## Concepts ### FILE: home/algopin-codes Title: AlgoPIN Codes Category: Concepts ---------------------------------------- # AlgoPIN Codes AlgoPIN generates valid PIN codes for a lock without WiFi, cellular, or remote access to the lock. ## How It Works ```mermaid sequenceDiagram Note over Your Server, Our Server: igloohome API Your Server->>Our Server: Request AlgoPIN Our Server-->>Your Server: AlgoPIN code Your Server->>Guest: AlgoPIN code Guest->>Lock: Enter AlgoPIN code Lock->>Lock: Unlock ``` ## Types of AlgoPIN Codes | Type | Validity | Variances | API | | --- | --- | --- | --- | | One-Time (OTP) | Single use | 10 | [Create a One-Time AlgoPIN](/home/api_access#create-a-one-time-algopin) | | Permanent | No end date | 10 | [Create a Permanent AlgoPIN](/home/api_access#create-a-permanent-algopin) | | Duration (Hourly) | 1 to 672 hours | 3 | [Create an Hourly AlgoPIN](/home/api_access#create-an-hourly-algopin) | | Duration (Daily) | 29 to 367 days | 3 | [Create a Daily AlgoPIN](/home/api_access#create-a-daily-algopin) | | Passage | 1 to 12 hours | 1 | [Create a Passage AlgoPIN](/home/api_access#create-a-passage-algopin) | A Passage AlgoPIN unlocks the lock and keeps it unlocked for the chosen number of hours. Passage requires newer lock firmware. ## Activation AlgoPIN codes valid for more than 24 hours need activation on the lock. The first time the code is used on the lock, it is activated. That first use must happen within the 24-hour activation window, which starts when the code's validity starts. The example below uses a code valid from 00:00, 1 Oct to 00:00, 5 Oct. The activation window is the first 24 hours of validity, from 00:00, 1 Oct to 00:00, 2 Oct. The timelines are not drawn to scale. Code validity The code is valid from 00:00 on 1 Oct to 00:00 on 5 Oct. The first 24 hours form the activation window. Code validity AlgoPIN code validity 24-hour window 00:00, 1 Oct 00:00, 2 Oct 00:00, 3 Oct 00:00, 5 Oct **First use within the activation window.** The code is activated, unlocks the lock, and stays valid until 00:00, 5 Oct. First use within the window The code is used for the first time inside the 24-hour window. The lock unlocks and the code is activated. First use within the window AlgoPIN code validity 24-hour window Used here Unlocks. Activated until 5 Oct. 00:00, 1 Oct 00:00, 5 Oct **First use after the activation window.** The code was not activated in the window. The lock does not unlock, even though the code is still within its validity period. First use after the window The code is used for the first time after the 24-hour window. The lock does not unlock because the code was not activated. First use after the window AlgoPIN code validity 24-hour window Used here Does not unlock. Not activated. 00:00, 1 Oct 00:00, 5 Oct ## AlgoPIN Limitations | Limitation | Explanation | | --- | --- | | AlgoPIN-supported devices only | The device has to support AlgoPIN. See [Supported Devices](/home/supported-devices). | | Paired devices | Pair the device with the igloohome app first. | | 24-hour activation | AlgoPIN codes valid for more than 24 hours need activation on the lock. The first use must be within the first 24 hours of validity. See [Activation](#activation). | | 256 rule | You can't delete an AlgoPIN whose start date is more than 256 hours after its creation. | | 199 rule | The lock's whitelist and blacklist hold at most 199 PIN codes in total (AlgoPIN and Bluetooth PIN codes). | | PINs per duration | For one duration, you can generate only as many PINs as the type's [variances](#variance-multiple-algopin-codes-for-the-same-duration). | ## Variance: Multiple AlgoPIN Codes for the Same Duration For a given duration, each variance returns a different PIN code. The Types table above lists the number of variances per type. One-Time and Permanent support 10 variances on locks with newer firmware. Some older locks gain this with a firmware update; others support 5. Example of hourly PIN codes for one lock, three variances each: | Start time | End time | PIN codes (variance 1, 2, 3) | deviceId | | --- | --- | --- | --- | | 2PM | 3PM | 511093918, 945844647, 052368115 | IGB403fe3b9d | | 2PM | 4PM | 633654730, 437755350, 337288611 | IGB403fe3b9d | | 3PM | 5PM | 297248165, 196981787, 384522901 | IGB403fe3b9d | | 3PM | 5PM | 948950871, 142404925, 817840781 | IGP114cde456 | The following request returns a unique PIN code (e.g. `4642226`): ```bash curl --request POST \ --url https://api.igloohome.co/home/devices/IGP101abc123/algopin/onetime \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer abcdef' \ --data '{ "variance": 1, "startDate": "2022-01-01T00:00:00+08:00", "accessName": "Maintenance guy" }' ``` Changing the variance returns a second unique PIN code (e.g. `1419472`) for the same duration: ```bash curl --request POST \ --url https://api.igloohome.co/home/devices/IGP101abc123/algopin/onetime \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer abcdef' \ --data '{ "variance": 2, "startDate": "2022-01-01T00:00:00+08:00", "accessName": "Maintenance guy 2" }' ``` ### FILE: home/iglookey Title: iglooKey Category: Concepts ---------------------------------------- # iglooKey iglooKey is a shareable digital key. Your application creates an iglooKey for one or more devices and sends the recipient a link. The recipient opens the link on their phone and unlocks the lock over Bluetooth. ## How It Works ```mermaid sequenceDiagram Note over Your Server, Our Server: igloohome API Your Server->>Our Server: Create iglooKey Our Server-->>Your Server: Shareable link Your Server->>Recipient: Shareable link Recipient->>Lock: Open link and unlock Lock->>Lock: Unlock ``` 1. Your application creates an iglooKey for the devices the recipient needs. 2. You send the shareable link to the recipient. 3. The recipient opens the link on their phone and unlocks the lock. 4. You can change the validity of an iglooKey, or revoke it when access must end. Create, update, and revoke iglooKeys with the iglooKey endpoints in the [Access API Reference](/home/api_access#iglookey). To be notified when an iglooKey is claimed or used, see [iglooKey Events](/home/iglookey-events). ## Validity | Setting | Explanation | | --- | --- | | Validity window | The iglooKey works from its start time until its end time. | | Activation window | Optional. The recipient must open the iglooKey within this time after the start time. | | Offline access | How long the iglooKey keeps working without an internet connection, up to 24 hours. Defaults to 24 hours. Once it runs out, the recipient's phone must go online to restore offline unlock, as long as the iglooKey is still within its validity window. | ## Online and Offline Unlock Every iglooKey supports both online and offline unlock. The recipient's phone checks for an internet connection at unlock time and uses the matching behavior: - **Offline:** The phone unlocks the lock. - **Online:** The phone unlocks the lock and also does the following: - Syncs the activity logs from the device. Each sync retrieves only a few logs, so a lock with many saved logs can take several online unlocks to clear its stored activity logs. - Reads the device battery level. ### FILE: home/supported-devices Title: Supported Devices Category: Concepts ---------------------------------------- # Supported Devices The igloohome API V2 provides programmatic access to manage igloohome smart locks, keypads, bridges, and accessories. This page covers all currently supported devices and their capabilities. ## Prerequisites - Valid igloohome API credentials - Understanding of RESTful API concepts - Device must be registered in your igloohome account ## Device ID Every igloohome device has a unique identifier called `deviceId`. This ID serves as the device's Bluetooth name and follows a specific 12-character format: `[PREFIX][BATCH][UNIQUE_ID]` | deviceId prefix | Batch number | Unique identifier | | --- | --- | --- | | 4 characters | 2 characters | 6 characters | | e.g. IGP1 | e.g. 01 | e.g. abc123 | Examples: ``` IGB403fe3b9d // Deadbolt 2S from batch 03 IGP1028de324 // Padlock from batch 02 OE1X01cd3a5f // Retrofit Lock from batch 01 ``` ## Device Type igloohome devices fall into four main categories: | Type | Description | | --- | --- | | Lock | Smart locks for doors, gates, and secure storage | | Keypad | Standalone keypads for code entry | | Bridge | Connectivity hubs for remote access | | Fob | Physical key fobs for device access | ## Supported Devices Devices marked with AlgoPIN support can generate time-sensitive PIN codes for temporary access. See [AlgoPIN Codes](/home/algopin-codes). To list your AlgoPIN-capable devices, call [Get Devices Supporting AlgoPIN](/home/api_devices#get-devices-supporting-algopin). *Last updated: October 15, 2026* ### Locks | Prefix | Device Name | AlgoPIN | | --- | --- | --- | | DBX1 | Deadbolt Go (Satin Nickel) | Yes | | DBX2 | Deadbolt Go (Matte Black) | Yes | | IGB4 | Deadbolt 2S Metal Grey | Yes | | IGB4A | Deadbolt 2E | Yes | | IGK3 | Keybox 3 | Yes | | IGM3 | Mortise 2 | Yes | | IGM4 | Mortise 2+ | Yes | | IGP1 | Padlock | Yes | | IGR1 | Rim Lock for Metal Gates | Yes | | ML5X | Smart Lever Mortise | Yes | | MP1F | Push-Pull Mortise | Yes | | MT1X | Mortise Touch | Yes | | OE1X | Retrofit Lock | No | | RG1X | Glass Door Lock | Yes | | RM2F | Gate Lock (with Fingerprint) | Yes | | RM2X | Gate Lock | Yes | | RW1X | Rim Lock for Wooden Doors | Yes | | SK3E | Keybox 3E | Yes | | SK4X | Keybox 4 | Yes | | SP1E | Padlock E | Yes | | SP2E | Padlock 2E | Yes | | SP2X | Padlock 2 | Yes | | SP3X | Padlock 3 | No | | SW1X | Switch | No | ### Accessories | Prefix | Device Name | AlgoPIN | | --- | --- | --- | | EB1X | Bridge | No | | EB2X | Bridge 2 | No | | EK1X | Keypad | Yes | | IEF1 | Key Fob | No | ### FILE: works/algopin-codes Title: AlgoPIN Codes Category: Concepts ---------------------------------------- # AlgoPIN Codes AlgoPIN generates valid PIN codes for a lock without WiFi, cellular, or remote access to the lock. ## How It Works ```mermaid sequenceDiagram Note over Your Server, Our Server: iglooworks API Your Server->>Our Server: Request AlgoPIN Our Server-->>Your Server: AlgoPIN code Your Server->>Guest: AlgoPIN code Guest->>Lock: Enter AlgoPIN code Lock->>Lock: Unlock ``` ## Types of AlgoPIN Codes | Type | Validity | Variances | API | | --- | --- | --- | --- | | One-Time (OTP) | Single use | 10 | [Create a One-Time AlgoPIN](/works/api_access#create-a-one-time-algopin) | | Permanent | No end date | 10 | [Create a Permanent AlgoPIN](/works/api_access#create-a-permanent-algopin) | | Duration (Hourly) | 1 to 672 hours | 3 | [Create an Hourly AlgoPIN](/works/api_access#create-an-hourly-algopin) | | Duration (Daily) | 29 to 367 days | 3 | [Create a Daily AlgoPIN](/works/api_access#create-a-daily-algopin) | | Passage | 1 to 12 hours | 1 | [Create a Passage AlgoPIN](/works/api_access#create-a-passage-algopin) | A Passage AlgoPIN unlocks the lock and keeps it unlocked for the chosen number of hours. Passage requires newer lock firmware. ## AlgoPIN Limitations | Limitation | Explanation | | --- | --- | | AlgoPIN-supported devices only | The device has to support AlgoPIN. See [Supported Devices](/works/supported-devices). | | Paired devices | Pair the device with the iglooworks app first. | | 24-hour activation | AlgoPIN codes valid for more than 24 hours need activation on the lock. Use the PIN code on the lock within the first 24 hours of its validity. | | 256 rule | You can't delete an AlgoPIN whose start date is more than 256 hours after its creation. | | 199 rule | The lock's whitelist and blacklist hold at most 199 PIN codes in total (AlgoPIN and Bluetooth PIN codes). | | PINs per duration | For one duration, you can generate only as many PINs as the type's [variances](#variance-multiple-algopin-codes-for-the-same-duration). | ## Variance: Multiple AlgoPIN Codes for the Same Duration For a given duration, each variance returns a different PIN code. The Types table above lists the number of variances per type. One-Time and Permanent support 10 variances on locks with newer firmware. Some older locks gain this with a firmware update; others support 5. Example of hourly PIN codes for one lock, three variances each: | Start time | End time | PIN codes (variance 1, 2, 3) | deviceId | | --- | --- | --- | --- | | 2PM | 3PM | 511093918, 945844647, 052368115 | IGB403fe3b9d | | 2PM | 4PM | 633654730, 437755350, 337288611 | IGB403fe3b9d | | 3PM | 5PM | 297248165, 196981787, 384522901 | IGB403fe3b9d | | 3PM | 5PM | 948950871, 142404925, 817840781 | IGP114cde456 | The following request returns a unique PIN code (e.g. `4642226`): ```bash curl --request POST \ --url https://api.igloohome.co/works/devices/IGP101abc123/algopin/onetime \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer abcdef' \ --data '{ "variance": 1, "startDate": "2022-01-01T00:00:00+08:00", "accessName": "Maintenance guy" }' ``` Changing the variance returns a second unique PIN code (e.g. `1419472`) for the same duration: ```bash curl --request POST \ --url https://api.igloohome.co/works/devices/IGP101abc123/algopin/onetime \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer abcdef' \ --data '{ "variance": 2, "startDate": "2022-01-01T00:00:00+08:00", "accessName": "Maintenance guy 2" }' ``` ### FILE: works/supported-devices Title: Supported Devices Category: Concepts ---------------------------------------- # Supported Devices ## Overview The iglooworks API V2 provides programmatic access to manage iglooworks smart locks, keypads, bridges, and accessories. This reference guide covers all currently supported devices and their capabilities. ### Prerequisites - Valid iglooworks API credentials - Understanding of RESTful API concepts - Device must be registered in your iglooworks account ## Device ID Every iglooworks device has a unique identifier called `deviceId`. This ID serves as the device's Bluetooth name and follows a specific 12-character format: ### Format Structure `[PREFIX][BATCH][UNIQUE_ID]` | deviceId prefix | Batch number | Unique identifier | | ------------------------ | ---------------------- | -------------------------- | | 4 characters | 2 characters | 6 characters | | e.g. IGP1 | e.g. 01 | e.g. abc123 | ### Examples ``` IGB403fe3b9d // Deadbolt 2S from batch 03 IGP1028de324 // Padlock from batch 02 OE1X01cd3a5f // Retrofit Lock from batch 01 ``` ## Device Type iglooworks devices fall into four main categories: | Type | Description | | ------ | ----------- | | Lock | Smart locks for doors, gates, and secure storage | | Keypad | Standalone keypads for code entry | | Bridge | Connectivity hubs for remote access | | Fob | Physical key fobs for device access | ## Supported Devices ### Current Device Support The following devices are supported in API V2. > **Last updated:** October 15, 2026 #### AlgoPIN Support Devices marked with AlgoPIN support can generate time-sensitive PIN codes for temporary access. See [AlgoPIN Codes](/works/algopin-codes). #### Locks | Prefix | Device Name | AlgoPIN | | ------ | ---------------------------- | ------- | | DBX1 | Deadbolt Go (Satin Nickel) | Yes | | DBX2 | Deadbolt Go (Matte Black) | Yes | | IGB4 | Deadbolt 2S Metal Grey | Yes | | IGB4A | Deadbolt 2E | Yes | | IGK3 | Keybox 3 | Yes | | IGM3 | Mortise 2 | Yes | | IGM4 | Mortise 2+ | Yes | | IGP1 | Padlock | Yes | | IGR1 | Rim Lock for Metal Gates | Yes | | ML5X | Smart Lever Mortise | Yes | | MP1F | Push-Pull Mortise | Yes | | MT1X | Mortise Touch | Yes | | OE1X | Retrofit Lock | No | | RM2F | Gate Lock (with Fingerprint) | Yes | | RM2X | Gate Lock | Yes | | RW1X | Rim Lock for Wooden Doors | Yes | | SK3E | Keybox 3E | Yes | | SK4X | Keybox 4 | Yes | | SP1E | Padlock E | Yes | | SP2E | Padlock 2E | Yes | | SP2X | Padlock 2 | Yes | | SP3X | Padlock 3 | No | | SW1X | Switch | No | #### Accessories | Prefix | Device Name | AlgoPIN | | ------ | ----------- | ------- | | EB1X | Bridge | No | | EB2X | Bridge 2 | No | | EK1X | Keypad | Yes | | IEF1 | Key Fob | No | ## Documentation ### FILE: home Title: Overview Category: Documentation ---------------------------------------- # igloohome Documentation The igloohome API manages igloohome smart locks from your backend, online or offline. AlgoPIN generates valid PIN codes without WiFi or cellular data on the lock. The API includes: - AlgoPIN codes - eKeys (Bluetooth keys) - iglooKey tokens - Access records - Remote control via a Bridge - Notifications via Webhooks ## HTTP Request Header Format | Key | Value | | --- | --- | | Authorization | `Bearer {access_token}` | | Content-Type | `application/json` | See [Individual Account](/home/individual-account) or [Partner Account](/home/partner-account) on how to get an access token. The igloohome API supports JSON only. Set `Accept` and `Content-Type` to `application/json`. ``` HTTP/1.1 Accept: application/json Content-Type: application/json ``` Example cURL: ```bash curl -X POST -H "Authorization: Bearer {access_token}" -H "Accept: application/json" -H "Content-Type: application/json" ``` ## Architecture igloohome supports two integration paths depending on who is authenticating: | | Individual account | Partner account | | --- | --- | --- | | Flow | OAuth 2.0 Client Credentials | OAuth 2.0 Authorization Code | | Who authenticates | Your own account | Each end user, individually | | Guide | [igloohome API](/home/individual-account) | [iglooconnect](/home/partner-account) | ### Your server to our server Server-to-server integrations (no end user login) use the OAuth 2.0 Client Credentials flow via **igloohome API**. Your backend authenticates directly with its own Client ID and Secret. See [Individual Account](/home/individual-account). ```mermaid sequenceDiagram participant Y as Your Server participant O as Our Server Note over O: igloohome API Y->>O: HTTP Request O-->>Y: HTTP Response ``` ### Bridge-connected devices Devices behind a Bridge are controlled through asynchronous bridge-proxied jobs: your server queues a job (lock, unlock, create PIN) against the Bridge, and the Bridge executes it against the device and reports status back via webhook. ```mermaid sequenceDiagram participant Y as Your Server participant O as Our Server participant B as Bridge participant D as Device Note over O: igloohome API Y->>O: Request O->>B: Request (WiFi) B->>D: Request (Bluetooth) D-->>B: Response B-->>O: Response O-->>Y: Response ``` ### FILE: Title: Redirecting Category: Documentation ---------------------------------------- Redirecting to the [igloohome documentation](/home)… ### FILE: works Title: Overview Category: Documentation ---------------------------------------- # iglooworks Documentation The iglooworks API manages iglooworks smart locks from your backend, online or offline. AlgoPIN generates valid PIN codes without WiFi or cellular data on the lock. The API includes: - AlgoPIN codes - eKeys (Bluetooth keys) - Access records - Users and departments - Remote control via a Bridge - Notifications via Webhooks ## HTTP Request Header Format | Key | Value | | --- | --- | | Authorization | `Bearer {access_token}` | | Content-Type | `application/json` | See [Individual Account](/works/individual-account) or [Partner Account](/works/partner-account) on how to get an access token. The iglooworks API supports JSON only. Set `Accept` and `Content-Type` to `application/json`. ``` HTTP/1.1 Accept: application/json Content-Type: application/json ``` Example cURL: ```bash curl -X POST -H "Authorization: Bearer {access_token}" -H "Accept: application/json" -H "Content-Type: application/json" ``` ## Architecture iglooworks supports two integration paths depending on who is authenticating: | | Individual account | Partner account | | --- | --- | --- | | Flow | OAuth 2.0 Client Credentials | OAuth 2.0 Authorization Code | | Who authenticates | Your own account | Each end user, per organization | | Guide | [Client credentials](/works/individual-account) | [Code flow](/works/partner-account) | ### Your server to our server Server-to-server integrations (no end user login) use the OAuth 2.0 Client Credentials flow via **iglooworks API**. Your backend authenticates directly with its own Client ID and Secret. See [Individual Account](/works/individual-account). ```mermaid sequenceDiagram participant Y as Your Server participant O as Our Server Note over O: iglooworks API Y->>O: HTTP Request O-->>Y: HTTP Response ``` ### Bridge-connected devices Devices behind a Bridge are controlled through asynchronous bridge-proxied jobs: your server queues a job (lock, unlock, create PIN) against the Bridge, and the Bridge executes it against the device and reports status back via webhook. ```mermaid sequenceDiagram participant Y as Your Server participant O as Our Server participant B as Bridge participant D as Device Note over O: iglooworks API Y->>O: Request O->>B: Request (WiFi) B->>D: Request (Bluetooth) D-->>B: Response B-->>O: Response O-->>Y: Response ``` ## Getting Started ### FILE: home/individual-account Title: Individual Account Category: Getting Started ---------------------------------------- # Individual Account ## Overview This guide helps you authenticate with the igloohome API using the OAuth 2.0 Client Credentials flow — for developers integrating their own igloohome account and devices, not anyone else's. You'll learn how to obtain access tokens and make your first API calls. Building on behalf of other igloohome account owners instead? See [Partner Account](/home/partner-account). ## Prerequisites - An igloohome account - Access to the igloohome API portal - Basic understanding of REST APIs and OAuth 2.0 ## Authentication ### OAuth 2.0 Client Credentials Flow The igloohome API uses the OAuth 2.0 Client Credentials flow for authentication. This flow is designed for server-to-server communication where your application acts on its own behalf. Reference: [OAuth 2.0 RFC 6749, section 4.4](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) ### Authentication Flow Diagram The following diagram illustrates the complete authentication process, from obtaining a token to using it and reauthenticating 24 hours later: ```mermaid sequenceDiagram participant App as Your Application participant Auth as Auth Server participant DB as Your Database participant API as igloohome API rect rgb(255, 245, 243) Note over App,Auth: 1. Authentication App->>Auth: POST /oauth2/token
scope='igloohomeapi/algopin-onetime' Auth-->>App: access_token, expires_in, token_type App->>DB: Store token + calculated expiry end rect rgb(248, 248, 248) Note over App,API: 2. API Usage App->>API: Create One-Time PIN
Authorization: Bearer {token} API-->>App: pin, pinId, success end rect rgb(255, 245, 243) Note over App,Auth: 3. New Token (24h later) App->>Auth: POST /oauth2/token (client_credentials) Auth-->>App: new access_token App->>DB: Update stored token end ``` ### Step 1: Obtain Your API Credentials - Log in to your [igloohome API portal](https://web.igloohome.co/api) - Navigate to the **API Access** section - Copy your **Client ID** and **Client Secret** New to igloohome? Connect your igloohome account to the igloohome API portal to start a 30-day free trial. ### Step 2: Encode Your Credentials Your credentials must be Base64 encoded before use: - Concatenate your Client ID and Client Secret with a colon: `client_id:client_secret` - Base64 encode the result: `Base64Encode(client_id:client_secret)` Example: ```javascript const clientId = "your_client_id_here"; const clientSecret = "your_client_secret_here"; const credentials = btoa(`${clientId}:${clientSecret}`); // Result: "eW91cl9jbGllbnRfaWRfaGVyZTp5b3VyX2NsaWVudF9zZWNyZXRfaGVyZQ==" ``` ```python import base64 credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() ``` ```go credentials := base64.StdEncoding.EncodeToString([]byte(clientID + ":" + clientSecret)) ``` ```bash echo -n "client_id:client_secret" | base64 ``` This encoded string will be used in the Authorization header as `Basic {credentials}`. ### Step 3: Request an Access Token Make a POST request to the token endpoint with your encoded credentials. **Endpoint:** `POST https://auth.igloohome.co/oauth2/token` #### Required Headers | Header | Value | Description | | --- | --- | --- | | Authorization | `Basic {credentials}` | Your Base64 encoded client credentials | | Content-Type | `application/x-www-form-urlencoded` | Required for form data | #### Required Parameters | Parameter | Value | Description | | --- | --- | --- | | grant_type | `client_credentials` | OAuth 2.0 grant type | #### Optional Parameters | Parameter | Description | | --- | --- | | scope | Space-separated list of permissions. If omitted, all permissions are granted. | #### Available Scopes | Scope | Description | | --- | --- | | igloohomeapi/algopin-permanent | Create permanent access AlgoPIN | | igloohomeapi/algopin-onetime | Create one-time access AlgoPIN | | igloohomeapi/algopin-daily | Create daily recurring AlgoPIN | | igloohomeapi/algopin-hourly | Create hourly recurring AlgoPIN | | igloohomeapi/create-pin-bridge-proxied-job | Create pins via bridge | | igloohomeapi/delete-pin-bridge-proxied-job | Delete pins via bridge | | igloohomeapi/lock-bridge-proxied-job | Lock devices via bridge | | igloohomeapi/unlock-bridge-proxied-job | Unlock devices via bridge | | igloohomeapi/get-devices | Retrieve device list | | igloohomeapi/update-device | Configure device settings | | igloohomeapi/get-master-pin | Get Master PIN | | igloohomeapi/get-properties | Get Properties | | igloohomeapi/get-device-status-bridge-proxied-job | Get device status | | igloohomeapi/get-battery-level-bridge-proxied-job | Get battery levels | | igloohomeapi/get-activity-logs-bridge-proxied-job | Get activity logs | | igloohomeapi/get-job-status | Get job status | | igloohomeapi/create-ekey-access | Generate bluetooth key | | igloohomeapi/get-device-activity | Get device activity log | | igloohomeapi/store-device-activity | Push device activity logs | #### Example Requests Request all permissions: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Authorization: Basic {your_encoded_credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials ``` Request specific permissions: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Authorization: Basic {your_encoded_credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=igloohomeapi/algopin-onetime igloohomeapi/get-devices' ``` ### Successful Response When authentication succeeds, you'll receive a JSON response with your access token: ```json { "access_token": "eyJraWQiOiJKc.....", "expires_in": 86400, "token_type": "Bearer" } ``` | Field | Description | | --- | --- | | access_token | JWT token for API authentication | | expires_in | Token lifetime in seconds (86400 = 24 hours) | | token_type | Always "Bearer" | ## Token Management Access tokens expire after 24 hours. Client credentials issue no refresh token, so when a token expires, reauthenticate by requesting a new access token with the same call. ### Best Practices - Store tokens securely - Calculate expiry time: `current_time + expires_in` - Request a new token shortly before the current one expires ## Implementation ### Using Your Token Include your access token in API requests: ```bash curl --request GET \ --url https://api.igloohome.co/home/devices \ --header 'Authorization: Bearer {your_access_token}' ``` ### Example 1: One-Time PIN Access For applications that only need to create one-time access pins, limit your scope for better security: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=igloohomeapi/algopin-onetime' ``` Note: Attempting to create permanent pins with a one-time scope token will result in `403 Forbidden`. ### Example 2: Device Management Application For applications that need full device control and monitoring: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=igloohomeapi/get-devices igloohomeapi/lock-bridge-proxied-job igloohomeapi/unlock-bridge-proxied-job igloohomeapi/get-device-status-bridge-proxied-job' ``` ### Example 3: Property Management System For comprehensive property management requiring all PIN types and device control: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=igloohomeapi/algopin-permanent igloohomeapi/algopin-onetime igloohomeapi/algopin-daily igloohomeapi/algopin-hourly igloohomeapi/get-devices igloohomeapi/lock-bridge-proxied-job igloohomeapi/unlock-bridge-proxied-job' ``` ## Quick Start Checklist ### Before You Begin - Sign in to the [igloohome API portal](https://web.igloohome.co/api) and open **API Access** - Copy your **Client ID** and **Client Secret** ### Implementation Steps - Base64 encode your Client ID and Client Secret - Request an access token with `grant_type=client_credentials` and the scopes your application needs - Call the API with the `Authorization: Bearer {access_token}` header - Reauthenticate when the token expires, 24 hours after issue ### Testing Your Integration - Request a token and confirm the response includes `access_token` and `expires_in` - Call `GET https://api.igloohome.co/home/devices` and confirm a 200 response ## Troubleshooting ### Error Responses When authentication fails, you'll receive an error response: ```json { "error": "error_type" } ``` ### Common Errors | Error Code | Cause | Solution | | --- | --- | --- | | invalid_request | Missing required parameter (e.g., grant_type) | Include all required parameters | | invalid_client | Invalid Client ID or Secret | Verify credentials in igloohome API portal | | invalid_scope | Requested scope doesn't exist | Check available scopes list | ### Status Codes **401 Unauthorized:** - Verify Base64 encoding is correct - Check Client ID and Secret are valid **400 Bad Request:** - Verify `grant_type=client_credentials` is included - Check Content-Type header is set correctly - Validate scope parameter format **402 Payment Required (on API calls):** - The account's igloohome API trial has ended, or its billing grace period has expired. Check the account's billing status. ### FILE: home/partner-account Title: Partner Account Category: Getting Started ---------------------------------------- # Partner Account ## Overview This guide helps you integrate with igloohome's API using OAuth 2.0 authentication via **iglooconnect** — for partners and integrators building on behalf of other igloohome account owners, whose end users log in and grant your application scoped access to their own locks. You'll learn how to: - Set up authentication with iglooconnect - Obtain and manage access tokens - Make your first API calls Managing only your own igloohome account and devices instead? See [Individual Account](/home/individual-account). ## Prerequisites Before starting, ensure you have: - A valid igloohome business partnership - Your callback URL(s) ready ## Authentication ### OAuth 2.0 Integration igloohome uses the OAuth 2.0 Authorization Code flow ([RFC 6749, section 4.1](https://www.rfc-editor.org/rfc/rfc6749#section-4.1)) for secure API access through [iglooconnect](https://connect.igloocompany.co/). This integration enables your users to authenticate with igloohome and grants your application access to their smart locks. ### Authentication Flow Diagram The following diagram illustrates the complete OAuth 2.0 authentication flow, from initial login through token refresh: ```mermaid sequenceDiagram participant User participant App as Your Application participant Auth as igloohome Auth Server participant API as igloohome API participant DB as Your Database rect rgb(255, 245, 243) Note over User,Auth: 1. Authentication Phase User->>App: Initiates login App->>Auth: Redirect to login page Auth->>User: Display login form User->>Auth: Enter credentials Auth->>App: Redirect with authorization code end rect rgb(248, 248, 248) Note over App,Auth: 2. Token Exchange App->>Auth: Exchange code for tokens Auth-->>App: Return access_token, refresh_token, id_token end rect rgb(255, 245, 243) Note over App,DB: 3. Token Management App->>DB: Store tokens securely App->>DB: Calculate and store expiry time end rect rgb(248, 248, 248) Note over App,API: 4. API Operations App->>API: GET /devices (with access_token) API-->>App: Return device list App->>API: POST /algopin (create permanent PIN) API-->>App: Return PIN and pinId end rect rgb(255, 245, 243) Note over App,Auth: 5. Token Refresh (when needed) App->>Auth: Refresh token request Auth-->>App: Return new access_token App->>DB: Update stored tokens end ``` ### Flow Steps Explained - **User Authentication**: The user initiates login within your app and is redirected to the igloohome secure login page. - **Authorization Code**: Upon successful authentication, the user is redirected back to your defined callback URL with a temporary authorization code. - **Token Exchange**: Your application exchanges this authorization code for long-lived access and refresh tokens. - **Secure Storage**: Tokens and their calculated expiration times are stored securely in your database. - **API Access**: The access token is used to authenticate requests to igloohome API endpoints. - **Token Refresh**: Your application proactively refreshes credentials before they expire to ensure uninterrupted service. ### Onboarding To integrate with iglooconnect, you must first register your application and obtain credentials. - **Prepare Configuration**: Determine your production callback URL(s). You can register multiple URLs if needed. - **Request Access**: Contact your Business Development representative or the integration team. - **Receive Credentials**: You will be issued a unique **Client ID** to identify your application. #### Static Redirect URI Your callback URLs **must be static** and use **HTTPS**. Dynamic URLs or plain HTTP are not supported for security reasons. - **Callback URL**: Must be HTTPS and exactly match one of the URLs registered with igloohome. - **Client ID**: The unique identifier provided during the onboarding process. ### Scopes and Permissions Scopes define what your application can access. Format: space-separated list of scope names. If no scopes are specified, all permissions are granted. #### Available Scopes | Scope | Description | | --- | --- | | igloohomeapi/algopin-permanent | Create permanent access AlgoPIN | | igloohomeapi/algopin-onetime | Create one-time access AlgoPIN | | igloohomeapi/algopin-daily | Create daily recurring AlgoPIN | | igloohomeapi/algopin-hourly | Create hourly recurring AlgoPIN | | igloohomeapi/create-pin-bridge-proxied-job | Create pins via bridge | | igloohomeapi/delete-pin-bridge-proxied-job | Delete pins via bridge | | igloohomeapi/lock-bridge-proxied-job | Lock devices via bridge | | igloohomeapi/unlock-bridge-proxied-job | Unlock devices via bridge | | igloohomeapi/get-devices | Retrieve device list | | igloohomeapi/update-device | Configure device settings | | igloohomeapi/get-master-pin | Get Master PIN | | igloohomeapi/get-properties | Get Properties | | igloohomeapi/get-device-status-bridge-proxied-job | Get device status | | igloohomeapi/get-battery-level-bridge-proxied-job | Get battery levels | | igloohomeapi/get-activity-logs-bridge-proxied-job | Get activity logs | | igloohomeapi/get-job-status | Get job status | | igloohomeapi/create-ekey-access | Generate bluetooth key | | igloohomeapi/get-device-activity | Get device activity log | | igloohomeapi/store-device-activity | Push device activity logs | ## Token Management Access tokens expire after 24 hours and must be refreshed using the refresh token. The refresh token itself expires 1 year after the initial login. ### Token Refresh Process When an access token expires, use the refresh token to obtain a new access token: ``` POST https://auth.igloohome.co/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic {your base64 encoded credentials} grant_type=refresh_token&client_id={CLIENT_ID}&refresh_token={REFRESH_TOKEN} ``` Example request: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic NThucjFwcjgyMTlj...' \ --data grant_type=refresh_token \ --data client_id=58nr1... \ --data refresh_token=dn43ud8uj32nk2je ``` ### Recommended Practices - **Expiry Tracking:** Calculate and store expiry timestamp for both access and refresh tokens - **Proactive Refresh:** Refresh access tokens before expiry (recommended 1 hour before) - **User Re-authentication:** Monitor refresh token expiry and prompt users to re-authenticate before the 1-year limit to prevent API access interruption When the refresh token nears expiration (recommended 30 days before), prompt users to re-authenticate through the login flow to maintain uninterrupted API access. ## Implementation ### Initiate Authentication Redirect users to igloohome's login page: ``` https://auth.igloohome.co/login?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPES} ``` #### Parameters | Parameter | Required | Description | | --- | --- | --- | | response_type | Yes | Must be `code` | | client_id | Yes | Your client ID from igloohome | | redirect_uri | Yes | Your callback URL (URL encoded) | | scope | Yes | Space-separated scope list | Example request: ```bash curl "https://auth.igloohome.co/login?client_id=your_client_id&response_type=code&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&scope=igloohomeapi%2Fget-devices" ``` #### Error Responses | Error Code | Description | Solution | | --- | --- | --- | | invalid_request | Missing or invalid parameter in the login URL (e.g., response_type, redirect_uri) | Check all required parameters are included | | unauthorized_client | Client is not allowed to use this flow | Verify your client with the igloohome team | | access_denied | The user declined the requested access | Ask the user to sign in and approve again | | invalid_scope | Requested scope not available | Use only supported scopes from the list above | ### Successful Authentication After successful authentication, the server redirects to your callback URL with an authorization code. ``` https://{REDIRECT_URI}?code={AUTH_CODE} ``` #### Exchange Authorization Code for Tokens Make a POST request to exchange the authorization code for access tokens: ``` POST https://auth.igloohome.co/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic {your base64 encoded credentials} grant_type=authorization_code&client_id={CLIENT_ID}&code={AUTH_CODE}&redirect_uri={REDIRECT_URI} ``` Example request: ```bash curl --request POST \ --url https://auth.igloohome.co/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic NThucjFwcjgyMTlj...' \ --data grant_type=authorization_code \ --data client_id=58nr1... \ --data code=86b48b3d-****-****-****-********** \ --data redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback ``` #### Success Response ```json { "id_token": "dmcxd329ujdmkemkd349r", "access_token": "eyJz9sdfsdfsdfsd", "refresh_token": "dn43ud8uj32nk2je", "token_type": "Bearer", "expires_in": 86400 } ``` Response fields: | Field | Description | | --- | --- | | id_token | Contains user identity claims (name, email) | | access_token | JWT token for API authentication | | refresh_token | Used to obtain new tokens (expires in 365 days) | | expires_in | Access token lifetime in seconds (86400 = 1 day) | | token_type | Always "Bearer" | #### Error Response ```json { "error": "invalid_request" } ``` | Error | Description | | --- | --- | | invalid_request | Missing required parameter (e.g., grant_type) | | invalid_client | Client authentication failed | | invalid_scope | Invalid scope requested | | invalid_grant | Authorization code expired or already used | ## Quick Start Checklist ### Before You Begin - Prepare HTTPS callback URL ### Implementation Steps - Redirect user to login page - Handle callback with authorization code - Exchange code for tokens - Store tokens securely - Make first API call - Implement token refresh logic ### Testing Your Integration - **Authentication Flow:** Verify login redirect works - **Token Exchange:** Confirm you receive valid tokens - **API Calls:** Test with minimal scopes first - **Token Refresh:** Verify refresh mechanism works ## Troubleshooting ### "Invalid redirect_uri" **Cause:** Callback URL mismatch **Solution:** Ensure the redirect_uri exactly matches one of the URLs registered with igloohome ### "403 Forbidden" on API calls **Cause:** Insufficient scopes **Solution:** Check that your access token includes the required scope for the API endpoint ### "402 Payment Required" on API calls **Cause:** The account's igloohome API trial has ended, or its billing grace period has expired **Solution:** Ask the account owner to check their billing status ### "Token expired" errors **Cause:** Access token expired (24-hour limit) **Solution:** Implement automatic token refresh using the refresh_token ### Getting Help - **Technical Issues:** Contact your BD representative - **Integration Support:** Email dev+support@igloohome.co ### FILE: works/individual-account Title: Individual Account Category: Getting Started ---------------------------------------- # Individual Account ## Overview This guide will help you authenticate with the iglooworks API using OAuth 2.0 Client Credentials flow. You'll learn how to obtain access tokens and make your first API calls. ## Prerequisites - An iglooworks account - Basic understanding of REST APIs and OAuth 2.0 ## Authentication ### OAuth 2.0 Client Credentials Flow The iglooworks API uses OAuth 2.0 Client Credentials flow for authentication. This flow is designed for server-to-server communication where your application acts on its own behalf. **Reference**: [OAuth 2.0 RFC 6749, section 4.4](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) ### Authentication Flow Diagram The following diagram illustrates the complete authentication process: ```mermaid sequenceDiagram participant App as Your Application participant Auth as Auth Server participant DB as Your Database participant API as iglooworks API Note over App, Auth: 1. Authentication App->>Auth: POST /oauth2/token
scope='iglooworksapi/algopin-onetime' Auth->>App: access_token, expires_in, token_type App->>DB: Store token + calculated expiry Note over App, API: 2. API Usage App->>API: Create One-Time PIN
Authorization: Bearer {token} API->>App: pin, pinId, success Note over App, API: 3. New Token (24h later) App->>Auth: POST /oauth2/token (client_credentials) Auth->>App: new access_token App->>DB: Update stored token ``` ### Step 1: Obtain Your API Credentials 1. Contact BD to obtain your api credentials ### Step 2: Encode Your Credentials Your credentials must be Base64 encoded before use: 1. Concatenate your Client ID and Client Secret with a colon: `client_id:client_secret` 2. Base64 encode the result: `Base64Encode(client_id:client_secret)` **Example:** ```javascript const clientId = "your_client_id_here"; const clientSecret = "your_client_secret_here"; const credentials = btoa(`${clientId}:${clientSecret}`); // Result: "eW91cl9jbGllbnRfaWRfaGVyZTp5b3VyX2NsaWVudF9zZWNyZXRfaGVyZQ==" ``` ```python import base64 client_id = "your_client_id_here" client_secret = "your_client_secret_here" credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() # Result: "eW91cl9jbGllbnRfaWRfaGVyZTp5b3VyX2NsaWVudF9zZWNyZXRfaGVyZQ==" ``` ```go package main import ( "encoding/base64" "fmt" ) func main() { clientId := "your_client_id_here" clientSecret := "your_client_secret_here" credentials := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) // Result: "eW91cl9jbGllbnRfaWRfaGVyZTp5b3VyX2NsaWVudF9zZWNyZXRfaGVyZQ==" } ``` ```bash # Using command substitution to encode credentials CLIENT_ID="your_client_id_here" CLIENT_SECRET="your_client_secret_here" CREDENTIALS=$(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64) echo $CREDENTIALS # Result: "eW91cl9jbGllbnRfaWRfaGVyZTp5b3VyX2NsaWVudF9zZWNyZXRfaGVyZQ==" ``` This encoded string will be used in the Authorization header as `Basic {credentials}`. ### Step 3: Request an Access Token Make a POST request to the token endpoint with your encoded credentials. **Endpoint:** `POST https://auth.iglooworks.co/oauth2/token` #### Required Headers | Header | Value | Description | | ----------------- | ------------------------------------- | ---------------------------------------- | | `Authorization` | `Basic {credentials}` | Your Base64 encoded client credentials | | `Content-Type` | `application/x-www-form-urlencoded` | Required for form data | #### Required Parameters | Parameter | Value | Description | | -------------- | ---------------------- | ---------------------- | | `grant_type` | `client_credentials` | OAuth 2.0 grant type | #### Optional Parameters | Parameter | Description | | ----------- | ------------------------------------------------------------------------------- | | `scope` | Space-separated list of permissions. If omitted, all permissions are granted. | #### Available Scopes | Scope | Description | | ------------------------------------------------------ | ---------------------------------------------- | | `iglooworksapi/create-custom-pin-permanent-job` | Create a job for a permanent custom PIN | | `iglooworksapi/create-custom-pin-duration-job` | Create a job for a duration-based custom PIN | | `iglooworksapi/create-custom-pin-onetime-job` | Create a job for a one-time custom PIN | | `iglooworksapi/unlock-bridge-proxied-job` | Unlock devices via bridge | | `iglooworksapi/lock-bridge-proxied-job` | Lock devices via bridge | | `iglooworksapi/create-pin-bridge-proxied-job` | Create custom PINs via bridge | | `iglooworksapi/delete-pin-bridge-proxied-job` | Delete custom PINs via bridge | | `iglooworksapi/delete-pin-job` | Delete a PIN access or revoke an eKey | | `iglooworksapi/get-devices` | Retrieve device list | | `iglooworksapi/get-job-status` | Get the status of a specific job | | `iglooworksapi/get-master-pin` | Get a device's admin key | | `iglooworksapi/get-properties` | Retrieve property list | | `iglooworksapi/get-access` | Get access records | | `iglooworksapi/get-battery-level-bridge-proxied-job` | Get battery levels via bridge | | `iglooworksapi/get-device-status-bridge-proxied-job` | Get device status via bridge | | `iglooworksapi/get-activity-logs-bridge-proxied-job` | Get activity logs via bridge | | `iglooworksapi/create-ekey-access` | Create eKey access | | `iglooworksapi/store-device-activity` | Store device activity | | `iglooworksapi/get-device-activity` | Get device activity | | `iglooworksapi/get-jobs` | Get a list of jobs | | `iglooworksapi/get-job-detail` | Get the details of a specific job | | `iglooworksapi/update-job-status` | Update the status of a job | | `iglooworksapi/algopin-permanent` | Create permanent access AlgoPINs | | `iglooworksapi/algopin-daily` | Create a daily AlgoPIN | | `iglooworksapi/algopin-hourly` | Create an hourly AlgoPIN | | `iglooworksapi/algopin-onetime` | Create a one-time AlgoPIN | | `iglooworksapi/get-departments` | Retrieve department list | | `iglooworksapi/get-account-detail` | Get account detail | #### Example Requests **Request all permissions:** ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Authorization: Basic {your_encoded_credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials ``` ```javascript const clientId = "your_client_id_here"; const clientSecret = "your_client_secret_here"; const encodedCredentials = btoa(`${clientId}:${clientSecret}`); const params = new URLSearchParams({ grant_type: 'client_credentials' }); const response = await fetch('https://auth.iglooworks.co/oauth2/token', { method: 'POST', headers: { 'Authorization': `Basic ${encodedCredentials}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: params }); ``` ```go package main import ( "encoding/base64" "net/http" "net/url" "strings" ) func main() { // Encode credentials clientId := "your_client_id_here" clientSecret := "your_client_secret_here" encodedCredentials := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) // Prepare form data data := url.Values{} data.Set("grant_type", "client_credentials") // Create request req, _ := http.NewRequest("POST", "https://auth.iglooworks.co/oauth2/token", strings.NewReader(data.Encode())) req.Header.Set("Authorization", "Basic "+encodedCredentials) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // Make the request client := &http.Client{} response, _ := client.Do(req) } ``` ```python import requests import base64 # Encode credentials client_id = "your_client_id_here" client_secret = "your_client_secret_here" encoded_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() # Make the request response = requests.post( 'https://auth.iglooworks.co/oauth2/token', headers={ 'Authorization': f'Basic {encoded_credentials}', 'Content-Type': 'application/x-www-form-urlencoded' }, data={ 'grant_type': 'client_credentials', } ) ``` **Request specific permissions:** ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Authorization: Basic {your_encoded_credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=iglooworksapi/algopin-onetime iglooworksapi/get-devices' ``` ```javascript const clientId = "your_client_id_here"; const clientSecret = "your_client_secret_here"; const encodedCredentials = btoa(`${clientId}:${clientSecret}`); const params = new URLSearchParams({ grant_type: 'client_credentials', scope: 'iglooworksapi/algopin-onetime iglooworksapi/get-devices' }); const response = await fetch('https://auth.iglooworks.co/oauth2/token', { method: 'POST', headers: { 'Authorization': `Basic ${encodedCredentials}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: params }); ``` ```go package main import ( "encoding/base64" "net/http" "net/url" "strings" ) func main() { // Encode credentials clientId := "your_client_id_here" clientSecret := "your_client_secret_here" encodedCredentials := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) // Prepare form data data := url.Values{} data.Set("grant_type", "client_credentials") data.Set("scope", "iglooworksapi/algopin-onetime iglooworksapi/get-devices") // Create request req, _ := http.NewRequest("POST", "https://auth.iglooworks.co/oauth2/token", strings.NewReader(data.Encode())) req.Header.Set("Authorization", "Basic "+encodedCredentials) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // Make the request client := &http.Client{} response, _ := client.Do(req) } ``` ```python import requests import base64 # Encode credentials client_id = "your_client_id_here" client_secret = "your_client_secret_here" encoded_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() # Make the request response = requests.post( 'https://auth.iglooworks.co/oauth2/token', headers={ 'Authorization': f'Basic {encoded_credentials}', 'Content-Type': 'application/x-www-form-urlencoded' }, data={ 'grant_type': 'client_credentials', 'scope': 'iglooworksapi/algopin-onetime iglooworksapi/get-devices' } ) ``` ### Successful Response When authentication succeeds, you'll receive a JSON response with your access token: ```json { "access_token": "eyJraWQiOiJKc.....", "expires_in": 86400, "token_type": "Bearer" } ``` | Field | Description | | ---------------- | ---------------------------------------------- | | `access_token` | JWT token for API authentication | | `expires_in` | Token lifetime in seconds (86400 = 24 hours) | | `token_type` | Always "Bearer" | ## Token Management Access tokens expire after 24 hours. Client credentials issue no refresh token, so when a token expires, reauthenticate by requesting a new access token with the same call. ### Best Practices - Store tokens securely - Calculate expiry time: `current_time + expires_in` - Request a new token shortly before the current one expires ## Implementation ### Using Your Token Include your access token in API requests: ```bash curl --request GET \ --url https://api.igloohome.co/works/devices \ --header 'Authorization: Bearer {your_access_token}' ``` ### Example 1: One-Time PIN Access For applications that only need to create one-time access pins, limit your scope for better security: ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=iglooworksapi/algopin-onetime' ``` > **Note:** Attempting to create permanent pins with a one-time scope token will result in `403 Forbidden`. ### Example 2: Device Management Application For applications that need full device control and monitoring: ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=iglooworksapi/get-devices iglooworksapi/lock-bridge-proxied-job iglooworksapi/unlock-bridge-proxied-job iglooworksapi/get-device-status-bridge-proxied-job' ``` ### Example 3: Property Management System For comprehensive property management requiring all PIN types and device control: ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Authorization: Basic {credentials}' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=client_credentials \ --data 'scope=iglooworksapi/algopin-permanent iglooworksapi/algopin-onetime iglooworksapi/algopin-daily iglooworksapi/algopin-hourly iglooworksapi/get-devices iglooworksapi/lock-bridge-proxied-job iglooworksapi/unlock-bridge-proxied-job' ``` ## Quick Start Checklist ### Before You Begin - Contact BD to obtain your API credentials ### Implementation Steps - Base64 encode your Client ID and Client Secret - Request an access token with `grant_type=client_credentials` and the scopes your application needs - Call the API with the `Authorization: Bearer {access_token}` header - Reauthenticate when the token expires, 24 hours after issue ### Testing Your Integration - Request a token and confirm the response includes `access_token` and `expires_in` - Call `GET https://api.igloohome.co/works/devices` and confirm a 200 response ## Troubleshooting ### Error Responses When authentication fails, you'll receive an error response: ```json { "error": "error_type" } ``` ### Common Errors | Error Code | Cause | Solution | | ------------------- | ------------------------------------------------ | --------------------------------------------- | | `invalid_request` | Missing required parameter (e.g.,`grant_type`) | Include all required parameters | | `invalid_client` | Invalid Client ID or Secret | Verify credentials with the iglooworks team | | `invalid_scope` | Requested scope doesn't exist | Check available scopes list | ### Status Codes **401 Unauthorized:** - Verify Base64 encoding is correct - Check Client ID and Secret are valid **400 Bad Request:** - Verify `grant_type=client_credentials` is included - Check Content-Type header is set correctly - Validate scope parameter format **402 Payment Required (on API calls):** - The account's iglooworks API trial has ended, or its billing grace period has expired. Check the account's billing status. ### FILE: works/partner-account Title: Partner Account Category: Getting Started ---------------------------------------- # Partner Account ## Overview This guide helps you integrate with iglooworks's API using OAuth 2.0 authentication. You'll learn how to: - Set up authentication - Obtain and manage access tokens - Make your first API calls ## Prerequisites Before starting, ensure you have: - A valid iglooworks business partnership - Your callback URL ready - Contact with your BD representative ## Authentication ### OAuth 2.0 Integration iglooworks api v2 uses OAuth 2.0 Authorization Code flow ([RFC 6749, section 4.1](https://www.rfc-editor.org/rfc/rfc6749#section-4.1)) for secure API access. This integration enables your users to authenticate with iglooworks account and grants your application access to their smart locks. ### Authentication Flow Diagram The following diagram illustrates the complete OAuth 2.0 authentication flow: ```mermaid sequenceDiagram participant User as User participant App as Your Application participant Auth as iglooworks Auth Server participant API as iglooworks API participant DB as Your Database Note over User, Auth: 1. Authentication Phase User->>App: Initiates login App->>Auth: Redirect to login page Auth->>User: Display login form User->>Auth: Enter credentials Auth->>App: Redirect with authorization code Note over App, Auth: 2. Token Exchange App->>Auth: Exchange code for tokens Auth->>App: Return access_token, refresh_token, id_token Note over App, DB: 3. Token Management App->>DB: Store tokens securely App->>DB: Calculate and store expiry time Note over App, API: 4. API Operations App->>API: GET /devices (with access_token) API->>App: Return device list App->>API: POST /algopin (create permanent PIN) API->>App: Return PIN and pinId Note over App, Auth: 5. Token Refresh (when needed) App->>Auth: Refresh token request Auth->>App: Return new access_token App->>DB: Update stored tokens ``` ### Flow Steps Explained 1. **User Authentication**: User clicks login in your app and is redirected to iglooworks's secure login page 2. **Authorization Code**: After successful login, user is redirected back with a temporary authorization code 3. **Token Exchange**: Your app exchanges the authorization code for long-lived tokens 4. **Secure Storage**: Store tokens and calculated expiry times in your secure database 5. **API Access**: Use the access token to make API calls to iglooworks services 6. **Token Refresh**: Proactively refresh tokens before they expire to maintain seamless access ### Getting Started **Current Process:** Manual onboarding (automated portal coming soon) **Required Steps:** 1. Contact your BD representative or our integration team 2. Provide your callback URL 3. Receive your client ID > #### Static Redirect URI > Your callback URL **must be static** and use HTTPS. Dynamic URLs or HTTP are not supported. **What You'll Need:** - Callback URL (must be HTTPS) - Client ID (provided by iglooworks) ### Scopes and Permissions Scopes define what your application can access. **Format** Space-separated list of scope names > #### Important > If no scopes are specified, all permissions are granted. #### Available Scopes | Scope | Description | |---|---| | `iglooworksapi/create-custom-pin-permanent-job` | Create a job for a permanent custom PIN | | `iglooworksapi/create-custom-pin-duration-job` | Create a job for a duration-based custom PIN | | `iglooworksapi/create-custom-pin-onetime-job` | Create a job for a one-time custom PIN | | `iglooworksapi/unlock-bridge-proxied-job` | Unlock devices via bridge | | `iglooworksapi/lock-bridge-proxied-job` | Lock devices via bridge | | `iglooworksapi/create-pin-bridge-proxied-job` | Create custom PINs via bridge | | `iglooworksapi/delete-pin-bridge-proxied-job` | Delete custom PINs via bridge | | `iglooworksapi/delete-pin-job` | Delete a PIN access or revoke an eKey | | `iglooworksapi/get-devices` | Retrieve device list | | `iglooworksapi/get-job-status` | Get the status of a specific job | | `iglooworksapi/get-master-pin` | Get a device's admin key | | `iglooworksapi/get-properties` | Retrieve property list | | `iglooworksapi/get-access` | Get access records | | `iglooworksapi/get-battery-level-bridge-proxied-job` | Get battery levels via bridge | | `iglooworksapi/get-device-status-bridge-proxied-job` | Get device status via bridge | | `iglooworksapi/get-activity-logs-bridge-proxied-job` | Get activity logs via bridge | | `iglooworksapi/create-ekey-access` | Create eKey access | | `iglooworksapi/store-device-activity` | Store device activity | | `iglooworksapi/get-device-activity` | Get device activity | | `iglooworksapi/get-jobs` | Get a list of jobs | | `iglooworksapi/get-job-detail` | Get the details of a specific job | | `iglooworksapi/update-job-status` | Update the status of a job | | `iglooworksapi/algopin-permanent` | Create permanent access AlgoPINs | | `iglooworksapi/algopin-daily` | Create a daily AlgoPIN | | `iglooworksapi/algopin-hourly` | Create an hourly AlgoPIN | | `iglooworksapi/algopin-onetime` | Create a one-time AlgoPIN | | `iglooworksapi/get-departments` | Retrieve department list | | `iglooworksapi/get-account-detail` | Get account detail | ## Token Management Access tokens expire after 24 hours and must be refreshed using the refresh token. The refresh token itself expires 1 year after the initial login. ### Token Refresh Process When an access token expires, use the refresh token to obtain a new access token: ```http POST https://auth.iglooworks.co/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic {your base64 encoded credentials} grant_type=refresh_token& client_id={CLIENT_ID}& refresh_token={REFRESH_TOKEN} ``` **Example Request:** ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic {your base64 encoded credentials}' \ --data grant_type=refresh_token \ --data client_id={CLIENT_ID} \ --data refresh_token={REFRESH_TOKEN} ``` ### Recommended Practices 1. **Expiry Tracking:** Calculate and store expiry timestamp for both access and refresh tokens 2. **Proactive Refresh:** Refresh access tokens before expiry (recommended 1 hour before) 3. **User Re-authentication:** Monitor refresh token expiry and prompt users to re-authenticate before the 1-year limit to prevent API access interruption > #### Important > When the refresh token nears expiration (recommended 30 days before), prompt users to re-authenticate through the login flow to maintain uninterrupted API access. ## Implementation ### Initiate Authentication Redirect users to iglooworks's login page: ```http https://auth.iglooworks.co/login?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPES} ``` **Parameters:** | Parameter | Required | Description | |-----------|----------|-------------| | `response_type` | Yes | Must be `code` | | `client_id` | Yes | Your client ID from iglooworks | | `redirect_uri` | Yes | Your callback URL (URL encoded) | | `scope` | Yes | Space-separated scope list | **Example Request:** ```bash curl "https://auth.iglooworks.co/login?client_id=your_client_id&response_type=code&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&scope=iglooworksapi%2Fget-devices" ``` #### Error Responses | Error Code | Description | Solution | |------------|-------------|----------| | `invalid_request` | Missing or invalid parameter in the login URL (e.g., `response_type`, `redirect_uri`) | Check all required parameters are included | | `unauthorized_client` | Client is not allowed to use this flow | Verify your client with the iglooworks team | | `access_denied` | The user declined the requested access | Ask the user to sign in and approve again | | `invalid_scope` | Requested scope not available | Use only supported scopes from the list above | ### Successful Authentication After successful authentication, the server redirects to your callback URL with an authorization code: ``` https://{redirect_uri}?code=AUTH_CODE ``` #### Exchange Authorization Code for Tokens Make a POST request to exchange the authorization code for access tokens: ```http POST https://auth.iglooworks.co/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic {your base64 encoded credentials} grant_type=authorization_code& client_id={CLIENT_ID}& code={AUTH_CODE}& redirect_uri={REDIRECT_URI} ``` **Example Request:** ```bash curl --request POST \ --url https://auth.iglooworks.co/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic {your base64 encoded credentials}' \ --data grant_type=authorization_code \ --data client_id={CLIENT_ID} \ --data code={AUTH_CODE} \ --data redirect_uri={REDIRECT_URI} ``` #### Success Response ```json { "id_token": "dmcxd329ujdmkemkd349r", "access_token": "eyJz9sdfsdfsdfsd", "refresh_token": "dn43ud8uj32nk2je", "token_type": "Bearer", "expires_in": 86400 } ``` **Response Fields** | Field | Description | |-------|-------------| | `id_token` | Contains user identity claims (name, email) | | `access_token` | JWT token for API authentication | | `refresh_token` | Used to obtain new tokens (expires in 365 days) | | `expires_in` | Access token lifetime in seconds (86400 = 1 day) | | `token_type` | Always "Bearer" | #### Error Response **Error Response Format:** ```json { "error": "invalid_request" } ``` **Error Types:** | Error | Description | |-------|-------------| | `invalid_request` | Missing required parameter (e.g., `grant_type`) | | `invalid_client` | Client authentication failed | | `invalid_scope` | Invalid scope requested | | `invalid_grant` | Authorization code expired or already used | ## Quick Start Checklist ### Before You Begin - Contact BD representative for client ID - Prepare HTTPS callback URL ### Implementation Steps 1. Redirect user to login page 2. Handle callback with authorization code 3. Exchange code for tokens 4. Store tokens securely 5. Make first API call 6. Implement token refresh logic ### Testing Your Integration 1. **Authentication Flow:** Verify login redirect works 2. **Token Exchange:** Confirm you receive valid tokens 3. **API Calls:** Test with minimal scopes first 5. **Token Refresh:** Verify refresh mechanism works ## Troubleshooting ### Common Issues #### "Invalid redirect_uri" **Cause:** Callback URL mismatch **Solution:** Ensure the redirect_uri exactly matches the URL registered with iglooworks #### "403 Forbidden" on API calls **Cause:** Insufficient scopes **Solution:** Check that your access token includes the required scope for the API endpoint #### "402 Payment Required" on API calls **Cause:** The account is billing-suspended, or its iglooworks API trial has ended **Solution:** Ask the account owner to check their billing status #### "Token expired" errors **Cause:** Access token expired (24-hour limit) **Solution:** Implement automatic token refresh using the refresh_token ### Getting Help - **Technical Issues:** Contact your BD representative - **Integration Support:** Email [dev+support@igloohome.co](mailto:dev+support@igloohome.co) ## Webhooks ### FILE: home/bridge-events Title: Bridge Events Category: Webhooks ---------------------------------------- # Bridge Events Bridge devices send webhook notifications for two kinds of events: events the Bridge generates automatically on its own, and events triggered by a job you sent to the Bridge (see [Lock Jobs](/home/api_jobs#lock-jobs) and [Bridge Jobs](/home/api_jobs#bridge-jobs)). All webhook requests are signed — see [Webhook Security](/home/webhook-security) for how to validate them. ## Bridge Automated Events These events are caused by the Bridge device itself and occur automatically, without user intervention. | Event Type | Number Mapping | | --- | --- | | Job Complete | 3 | | Activity Log Received | 5 | | Bridge Connection | 10 | ### Job Complete Event ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISODATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` `type` is always `3` for Job Complete events. `data` also carries job-specific fields — see [Job Result Payloads](/home/job-result-payloads). `proxyId` is the Bridge device ID that relayed the command; `product.id` is the target lock device ID. **Job Status Codes** | Code | Status | Description | | --- | --- | --- | | 0 | Success | Job completed successfully | | 1 | Generic Error | Job failed due to generic error | | 2 | Operation In Progress | Another operation is currently running on the device | **Supported Job Types** The following job types trigger Job Complete Event webhooks: | Job Type ID | Operation Name | Description | | --- | --- | --- | | 1 | Lock | Engage the lock mechanism | | 2 | Unlock | Disengage the lock mechanism | | 4 | Create PIN | Add a new custom PIN code to the device | | 5 | Delete PIN | Remove an existing custom PIN code | | 9 | Get Battery Level | Retrieve current battery percentage | | 10 | Get Status | Fetch current lock state and diagnostics | | 15 | Get Logs | Get device activity logs | ### Activity Log Received Event ```json { "payload": { "event": { "id": "string", "type": 5, "data": { "activityLogs": [ { "logType": 0, "entryDate": 0, "operationId": 0, "keyId": 0, "pin": "string", "newPin": "string", "pinType": 0, "value1": 0, "value2": 0, "value3": 0, "value4": 0, "value5": 0, "time1": 0, "time2": 0 } ] }, "date": "ISODATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` `activityLogs` is an array of lock activity objects. Fields on each activity object other than `pin`, `newPin`, `pinType`, and `value1`-`value5`/`time1`-`time2` are always present; those are populated only for the log types that use them. Every lock activity object has these common fields: | Common Field | Description | | --- | --- | | logType | An integer indicating the type of event | | entryDate | The time since UNIX epoch in seconds that the event occurred | | keyId | Identifier of the eKey used for guest operations, or 0 if it was an event initiated via a Bluetooth Admin key or via the lock's keypad | | operationId | An identifier of the Bluetooth command operation that generated this activity log, or undefined if there was no operationId defined for that particular command | Some log types are populated with additional fields: | Log Type | Translation | Additional Fields | Description | | --- | --- | --- | --- | | 11 | Bluetooth unlock | N/A | N/A | | 13 | Bluetooth set time | time1 | The time the clock was set to | | 14 | Change volume via keypad | value1, value2 | Previous volume, new volume | | 15 | Change volume via Bluetooth | value1, value2 | Previous volume, new volume | | 16 | Wrong PIN | pin | The wrong PIN that was attempted | | 17 | PIN exceeded maximum length | pin | The PIN that was attempted | | 18 | Unlock with master PIN | pin | The master PIN | | 19 | Unlock with generated PIN previously used at least once (permanent, duration, weekly etc) | pin | The PIN used to unlock | | 20 | Use a one-time PIN | pin | The PIN that was used | | 21 | Use a permanent PIN for the first time | pin | The PIN that was used | | 22 | Use a duration PIN for the first time | pin | The PIN that was used | | 23 | Use a weekly PIN for the first time | pin | The PIN that was used | | 24 | Use a monthly PIN for the first time | pin | The PIN that was used | | 25 | Use a weekday PIN for the first time | pin | The PIN that was used | | 26 | Use a weekend PIN for the first time | pin | The PIN that was used | | 27 | Create a new PIN via Bluetooth | pin, time1, time2, pinType | The PIN that was used; start time if sent via Bluetooth; end time if sent via Bluetooth; the type of PIN used | | 28 | Change PIN via keypad | pin, newPin | The original PIN; the new PIN it is changed to | | 29 | Change PIN via Bluetooth | pin, newPin, time1, time2, pinType | The original PIN; the new PIN, if sent via Bluetooth; start/end time if sent via Bluetooth; the PIN type if sent via Bluetooth | | 30 | Delete PIN via keypad | pin | The PIN that was deleted | | 31 | Delete PIN via Bluetooth | pin | The PIN that was deleted | | 32 | Change Master PIN | pin, newPin | The old master PIN; the new master PIN | | 33 | Change auto-relock via keypad | value1, value2 | Time of timer autorelock in seconds; time of sensor autorelock in seconds | | 34 | Change auto-relock via Bluetooth | value1 | Time of sensor autorelock in seconds, or 0 if disabled | | 35 | Change incorrect PIN lockout count via keypad | value1, value2 | Previous value of incorrect PIN lockout count; new value | | 36 | Change incorrect PIN lockout count via Bluetooth | value1, value2 | Previous value, or 0 if disabled; new value, or 0 if disabled | | 37 | Bluetooth lock | N/A | N/A | | 38 | Enable auto-unlock | value1, value2 | 1 if enabled, 0 if disabled; minimum RSSI to unlock when touched, actual RSSI value = value2 + 256 | | 39 | Blacklist eKey | value3 | eKey ID | | 40 | Unblacklist eKey | value3 | eKey ID | | 41 | Change daylight savings time | N/A | N/A | | 42 | Add key card | value5 | Card UID | | 43 | Key card unlock | value5 | Card UID | | 44 | Delete key card | value5 | Card UID | | 45 | Set LED brightness | value1 | Brightness | | 46 | Set relock alarm | value1 | Time before alarm sounds after opening in seconds, or 0 if disabled | | 47 | Auto-relock lock (after opening and closing the lock) | N/A | N/A | | 48 | Auto-relock lock (without first opening the lock) | N/A | N/A | | 49 | Lock via keypad/button | N/A | N/A | | 50 | Unlock via button | N/A | N/A | | 51 | Lock with key/thumbturn | N/A | N/A | | 52 | Unlock with key/thumbturn | N/A | N/A | | 53 | Attempted break-in | N/A | N/A | | 54 | Add fingerprint | value2 | Fingerprint UID | | 55 | Delete fingerprint | value2 | Fingerprint UID | | 56 | Fingerprint unlock | value1, value2 | 0 if unlock was successful, else a Bluetooth command result code; fingerprint UID (null if unsuccessful) | | 57 | Invalid key card unlock attempt | value5 | Card UID | ### Bridge Connection Event Sent when a Bridge (EB1X) connects to or disconnects from the server. Use it to update device status, trigger alerts, or drive automation when connectivity changes. ```json { "payload": { "event": { "id": "string", "type": 10, "data": { "isOnline": true }, "date": "ISODATE", "proxyTransportMethod": 2 }, "product": { "id": "string", "type": 1 } } } ``` `type` is always `10` for Bridge Connection events. `data.isOnline` is `true` when the bridge is online (connected), `false` when offline (disconnected). `product.id` is the Bridge device ID. ## Bridge Targeted Events Bridge Targeted Events are sent when a Bridge device processes a job that targets the Bridge itself (see [Bridge Jobs](/home/api_jobs#bridge-jobs)). | Event Type | Number Mapping | | --- | --- | | Job Processed | 3 | ### Get Device Status ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "wifiNetworkStatus": 0, "wifiNetworkSsid": "string", "wifiNetworkIpaddr": "string", "wifiNetworkDns": "string", "wifiNetworkChannel": 0, "wifiNetworkBand": 0, "wifiNetworkRssi": 0, "lockRssiList": [0], "locksMaxCapacity": 0, "locksCurrentAdded": 0 }, "date": "ISODATE", "proxyTransportMethod": 0 }, "product": { "id": "string", "type": 2 } } } ``` **Job Status** | Job Status | Number Mapping | | --- | --- | | Success | 0 | | Generic failure | 1 | | Another operation is in progress | 2 | **WiFi Network Status** | WiFi Network Status | Number Mapping | | --- | --- | | Connected | 0 | | Disconnected/SSID not found/Invalid Password | 2 | | No Internet Connectivity | 5 | ### FILE: home/iglookey-events Title: iglooKey Events Category: Webhooks ---------------------------------------- # iglooKey Events igloohome sends webhook notifications when a recipient claims a token and when a recipient unlocks a lock with it. See [iglooKey](/home/iglookey) for how tokens are created and shared. All webhook requests are signed — see [Webhook Security](/home/webhook-security) for how to validate them. ## Delivery Each event is sent as an HTTP `POST` to your registered webhook URL with `Content-Type: application/json` and an `x-igloocompany-sha256` signature header. Events are sent to: - The account that owns the iglooKey token. - Apps the owner has connected through OAuth. See [Partner Account](/home/partner-account). ## Event Types | Event Type | Number Mapping | | --- | --- | | iglooKey Claimed | 1000 | | iglooKey Used | 1001 | Every event shares these fields: | Field | Type | Description | | --- | --- | --- | | id | string | Unique event ID (UUID) | | type | integer | Event type number | | data | object | Event-specific fields | | date | string | When the webhook was sent, in ISO 8601. This is not the claim or unlock time. | | proxyTransportMethod | integer | Always `0` | All timestamps are ISO 8601 strings in UTC with milliseconds (e.g. `2025-09-15T10:30:45.000Z`). ## iglooKey Claimed Event Sent once, when a recipient first claims an iglooKey token. It is not sent when: - The recipient claims a token that is already claimed. - The recipient claims an expired token. - The recipient claims a token whose access window has ended. ```json { "payload": { "event": { "id": "3f6c2a9e-8b41-4d7a-9e15-2c7d4b8a6f01", "type": 1000, "data": { "tokenId": "lnk_abc123def456ghi789jkl012mno345", "claimedAt": "2025-09-15T10:30:45.000Z" }, "date": "2025-09-15T10:30:45.318Z", "proxyTransportMethod": 0 } } } ``` `type` is always `1000` for iglooKey Claimed events. This event has no `product` object. | Field | Type | Description | | --- | --- | --- | | tokenId | string | The iglooKey token | | claimedAt | string | When the token was claimed | ## iglooKey Used Event Sent each time a recipient unlocks a lock with an iglooKey token. Each event covers one lock. - **Online unlock:** Sent when the unlock completes while the recipient's phone is online. - **Offline unlock:** Sent when the recipient's phone is back online and reports the unlock. The event can arrive well after the unlock. This example is an offline unlock reported about 95 minutes after it happened: ```json { "payload": { "event": { "id": "9a4e7c12-5d3b-4f86-b2a0-71e8c6d93f45", "type": 1001, "data": { "tokenId": "lnk_abc123def456ghi789jkl012mno345", "bluetoothId": "IGP1092b1e66", "unlockedAt": "2025-09-16T08:12:03.000Z", "reportedAt": "2025-09-16T09:47:20.000Z" }, "date": "2025-09-16T09:47:20.540Z", "proxyTransportMethod": 0 }, "product": { "id": "IGP1092b1e66", "type": 1 } } } ``` `type` is always `1001` for iglooKey Used events. `product.id` is the Bluetooth ID of the unlocked device. | Field | Type | Description | | --- | --- | --- | | tokenId | string | The iglooKey token | | bluetoothId | string | Bluetooth ID of the unlocked device | | unlockedAt | string | When the unlock happened | | reportedAt | string | When the server received the unlock | **Product Type** | Product Type | Number Mapping | | --- | --- | | Lock | 1 | | Keypad (`bluetoothId` starts with `EK`) | 3 | ## Handling Events - **Ordering:** Events are not guaranteed to arrive in order. - **Offline delay:** For offline unlocks, `reportedAt` can be much later than `unlockedAt`. Use `unlockedAt` for when access happened. ### FILE: home/job-result-payloads Title: Job Result Payloads Category: Webhooks ---------------------------------------- # Job Result Payloads This document provides detailed information about the structure and meaning of webhook payloads sent in response to various job requests. Each payload includes metadata about the event and the product involved, along with job-specific data such as status codes, battery levels, or device states. ## Overview Webhooks are used to notify your system of the outcome of asynchronous jobs initiated via API. Each webhook payload contains an `event` object that describes the job result and a `product` object identifying the device associated with the job. All timestamps are in ISO 8601 format (e.g., `2023-04-05T12:34:56Z`), `proxyTransportMethod` is `2` (sent through a Bridge), and `proxyId` is the ID of the Bridge that relayed the job. ## Common Job Status Codes The following status codes are shared across all job types unless otherwise specified: | Job Status | Number Mapping | | -------------------------------- | -------------- | | Success | `0` | | Generic failure | `1` | | Another operation is in progress | `2` | ## Lock Device ### Description This job locks the device. The webhook payload indicates whether the operation was successful. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ## Unlock Device ### Description This job unlocks the device. The webhook payload reflects the result of the unlock operation. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ## Create Custom PIN ### Description This job creates a custom PIN on the device. The webhook payload provides feedback on the creation attempt. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Status Codes | Job Status | Number Mapping | | -------------------------------- | -------------- | | Invalid Custom PIN length | `5` | | Duplicate PIN | `9` | | Insufficient PIN Storage | `12` | ## Delete PIN ### Description This job deletes a PIN from the device. The webhook payload indicates whether the deletion was successful or if the PIN was not found. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Status Codes | Job Status | Number Mapping | | -------------------------------- | -------------- | | PIN not found | `3` | ## Get Battery Level ### Description This job retrieves the current battery level of the device. The webhook payload includes both the job status and the battery level as a percentage. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "batteryLevel": 75 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Job Data | Device Battery Level Range | Description | | -------------------------- | ---------------------------------------- | | 0 - 100 | Represents battery level as a percentage | ## Get Device Status ### Description This job retrieves the current status of the device, including whether the lock and door are open. The webhook payload includes the job status and the device state. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "lockOpen": false, "doorOpen": true }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Lock and Door States | Lock Open | Boolean Mapping | | -------------------------- | --------------- | | Locking Mechanism Not Open | `false` | | Locking Mechanism Open | `true` | | Door Open | Boolean Mapping | | ----------- | --------------- | | Door Closed | `false` | | Door Open | `true` | ## Get Activity Logs ### Description A Get Activity Logs job (`jobType` 15) produces two separate webhooks: - A Job Complete event (`type: 3`). Its `data` carries `jobId`, `jobStatus`, the raw log blob in `data`, and `hasMore`. It does not contain decoded log entries. - An [Activity Log Received event](/home/bridge-events#activity-log-received-event) (`type: 5`) with the decoded entries in `activityLogs`. It is sent only when the lock reported new log entries. The two events can arrive in either order. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "data": "string", "hasMore": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### FILE: home/webhook-security Title: Webhook Security Category: Webhooks ---------------------------------------- # Webhook Security Each webhook request carries its own signature in the header. Requests are signed with an RSA key pair unique to your account, created when you add your first webhook URL. Validate the signature with your account's public key to confirm the request is authentic. Your server must accept `POST` requests — all webhook notifications are sent via `POST`. ## Setting Up Your Webhook URL ### igloohome API Partners Add your webhook URL on the [igloohome API](https://web.igloohome.co/api) portal, in the **Webhooks** section. ### iglooconnect Partners Email `dev+support@igloohome.co` to get your webhook URL set up — we'll add it right away. An iglooconnect portal for self-service webhook URL management is planned. ## Signature Process Part of the webhook request is passed through HMAC, and the resulting digest is signed with your account's RSA private key. ## Webhook Request Validation Given the following HTTPS request: ``` POST /example/path HTTP/1.1 Host: example-host.com Date: Sat, 20 Jun 2015 12:34:56 GMT Accept: application/json, text/plain, */* Content-Type: application/json Content-Length: 29 User-Agent: igloohome x-igloocompany-sha256: QA...== {"payload":"example payload"} ``` The webhook request signature is carried in the `x-igloocompany-sha256` header, base64-encoded. The body above is shortened. A real body is a JSON event wrapped in `payload`, as shown on [Bridge Events](/home/bridge-events) and [iglooKey Events](/home/iglookey-events). ### Signed Data Construction Construct the signed data from the following fields, extracted from the request, in order: - Method (all uppercase) - Host (hostname only, without port) - URL Path (without query string) - Content Type (always `application/json`) - Date - Body Host and URL Path come from your registered webhook URL, which is stored in lowercase. Concatenate each item, delimited by the bar (`|`) character. For the request above, the final concatenated payload is: ``` POST|example-host.com|/example/path|application/json|Sat, 20 Jun 2015 12:34:56 GMT|{"payload":"example payload"} ``` ### HMAC Process Hash the constructed signed data using HMAC with the SHA-256 hashing algorithm to get the HMAC digest. Use the decoded public key (see [Public Key](#public-key)) as the HMAC key. ### Signature Validation Decode the signature from the header and verify it against the HMAC digest with your public key. The signature is RSA PKCS#1 v1.5 with SHA-256. ### Public Key | Property | Value | | --- | --- | | Length | 2048 | | Cipher | RSA | | Format | DER | | Type | PKCS1 | Contact `dev+support@igloohome.co` to request your account's public key. The key you receive is base64-encoded. ## Working Example The following Node.js example (`crypto` library, v16.x LTS) validates the signature of an example payload with a sample public key: ```javascript // Using nodejs crypto library. import * as crypto from 'crypto'; // The example payload that we'll be validating. const payload = 'POST|example-host.com|/example/path|application/json|Sat, 20 Jun 2015 12:34:56 GMT|{"payload":"example payload"}'; // We use SHA-256 hash algorithm in both the HMAC computation and signature validation. const hashAlgorithm = 'sha256'; // Decode both the public key string and signature string. // (Assuming `publicKeyString` and `signatureString` are both valid.) const publicKey = Buffer.from(publicKeyString, 'base64'); const signature = Buffer.from(signatureString, 'base64'); // Compute the hash using HMAC. The body is hashed as UTF-8 bytes. const hmacDigest = crypto.createHmac(hashAlgorithm, publicKey) .update(payload, 'utf8') .digest(); // Validate signature. const result = crypto.verify( hashAlgorithm, hmacDigest, crypto.createPublicKey({ key: publicKey, format: 'der', type: 'pkcs1' }), signature ); // Output validation result. console.log('signature valid:', result); // signature valid: true ``` ### FILE: works/bridge-events Title: Bridge Events Category: Webhooks ---------------------------------------- # Bridge Events These events are caused by the bridge device itself and occur automatically without user intervention. | Event Type | Number Mapping | | --------------------- | -------------- | | [Job Complete](#job-complete-event) | 3 | | [Activity Log Received](#activity-log-received-event) | 5 | | [Bridge Connection](#bridge-connection-event) | 10 | ## Activity Log Received Event ### Expected Payload ```json payload: { event: { id: string, type: 5, data: { activityLogs: [ // Array of lock activity objects { // Lock activity object 1 logType: number, entryDate: number, // epoch seconds operationId: number, keyId: number, pin?: string, newPin?: string, pinType?: number, value1?: number, value2?: number, value3?: number, value4?: number, value5?: number, time1?: number, time2?: number }, // More lock activity objects {...}, {...} ] }, date: ISO_DATE, proxyTransportMethod: 2, proxyId: string }, product: { id: string, type: 1 } } ``` ### Event Fields Every lock event will have the following common fields: | Common Field | Description | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | logType | An integer indicating the type of event | | entryDate | The time since UNIX epoch in seconds that the event occurred | | keyId | Identifier of the eKey used for guest operations, or 0 if it was an event initiated via a Bluetooth Admin key or via the lock's keypad | | operationId | An identifier of the Bluetooth command operation that generated this activity log, or undefined if there was no operationId defined for that particular command | Additionally, some log types are populated with additional fields: | Log Type | Translation | Additional Fields | Description | | -------- | ----------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------- | | 11 | Bluetooth unlock | N/A | N/A | | 13 | Bluetooth set time | time1 | The time the clock was set to | | 14 | Change volume via keypad | value1 | Previous volume | | | | value2 | New volume | | 15 | Change volume via Bluetooth | value1 | Previous volume | | | | value2 | New volume | | 16 | Wrong PIN | pin | The wrong PIN that was attempted | | 17 | PIN exceeded maximum length | pin | The PIN that was attempted | | 18 | Unlock with master PIN | pin | The master PIN | | 19 | Unlock with generated PIN previously used at least once (permanent, duration, weekly etc) | pin | The PIN used to unlock | | 20 | Use a one-time PIN | pin | The PIN that was used | | 21 | Use a permanent PIN for the first time | pin | The PIN that was used | | 22 | Use a duration PIN for the first time | pin | The PIN that was used | | 23 | Use a weekly PIN for the first time | pin | The PIN that was used | | 24 | Use a monthly PIN for the first time | pin | The PIN that was used | | 25 | Use a weekday PIN for the first time | pin | The PIN that was used | | 26 | Use a weekend PIN for the first time | pin | The PIN that was used | | 27 | Create a new PIN via Bluetooth | pin | The PIN that was used | | | | time1 | Start time, if it was sent via Bluetooth | | | | time2 | End time, if it was sent via Bluetooth | | | | pinType | The type of PIN used | | 28 | Change PIN via keypad | pin | The original PIN | | | | newPin | The new PIN it is changed to | | 29 | Change PIN via Bluetooth | pin | The original PIN | | | | newPin | The new PIN it is changed to, if it was sent via Bluetooth | | | | time1 | Start time, if it was sent via Bluetooth | | | | time2 | End time, if it was sent via Bluetooth | | | | pinType | The type of PIN it is set to, if it was sent via Bluetooth | | 30 | Delete PIN via keypad | pin | The PIN that was deleted | | 31 | Delete PIN via Bluetooth | pin | The PIN that was deleted | | 32 | Change Master PIN | pin | The old master PIN | | | | newPin | The new master PIN | | 33 | Change auto-relock via keypad | value1 | Time of timer autorelock in seconds | | | | value2 | Time of sensor autorelock in seconds | | 34 | Change auto-relock via Bluetooth | value1 | Time of sensor autorelock in seconds, or 0 if disabled | | 35 | Change incorrect PIN lockout count via keypad | value1 | Previous value of incorrect PIN lockout count | | | | value2 | New value of incorrect PIN lockout count | | 36 | Change incorrect PIN lockout count via Bluetooth | value1 | Previous value of incorrect PIN lockout count, or 0 if disabled | | | | value2 | New value of incorrect PIN lockout count, or 0 if disabled | | 37 | Bluetooth lock | N/A | N/A | | 38 | Enable auto-unlock | value1 | 1 if enabled, 0 if disabled | | | | value2 | Minimum RSSI to unlock when touched, actual RSSI value = value2 + 256 | | 39 | Blacklist eKey | value3 | eKey ID | | 40 | Unblacklist eKey | value3 | eKey ID | | 41 | Change daylight savings time | N/A | N/A | | 42 | Add key card | value5 | Card UID | | 43 | Key card unlock | value5 | Card UID | | 44 | Delete key card | value5 | Card UID | | 45 | Set LED brightness | value1 | Brightness | | 46 | Set relock alarm | value1 | Time before alarm sounds after opening in seconds, or 0 if disabled | | 47 | Auto-relock lock (after opening and closing the lock) | N/A | N/A | | 48 | Auto-relock lock (without first opening the lock) | N/A | N/A | | 49 | Lock via keypad/button | N/A | N/A | | 50 | Unlock via button | N/A | N/A | | 51 | Lock with key/thumbturn | N/A | N/A | | 52 | Unlock with key/thumbturn | N/A | N/A | | 53 | Attempted break-in | N/A | N/A | | 54 | Add fingerprint | value2 | Fingerprint UID | | 55 | Delete fingerprint | value2 | Fingerprint UID | | 56 | Fingerprint unlock | value1 | 0 if unlock was successful, if not successful, refer to Bluetooth commands result codes | | | | value2 | Fingerprint UID (null if the unlock attempt was unsuccessful) | | 57 | Invalid key card unlock attempt | value5 | Card UID | | 93 | Enable DPI | value1 | 1 if enabled, 0 if disabled | | 77 | Enable passage mode | value3 | 1 if enabled, 0 if disabled | | | | value4 | Mode of the settings
1 - Power Mode
2 - Scheduling Mode
3 - Night Mode
4 - Passage Mode| | | | value5 | Information passage mode (start_time+end_time+weekdaymask)
Example:start_time: 34200(0x00008598);end_time: 63000(0000f618);weekday_mask: 64(0x40); value5 = bytes.fromhex(”00008598 0000f618 40”) | ## Job Complete Event ### Expected Payload ```json { "payload": { "event": { // ID for this webhook event "id": "string", // Always 3 for Job Complete events "type": 3, "data": { "jobId": "string", // Status code (see Job Status Codes below) "jobStatus": 0 ...job specific data }, // ISO 8601 timestamp of job completion "date": "ISODATE", "proxyTransportMethod": 2, // Bridge device ID that relayed the command "proxyId": "string" }, "product": { // Target lock device ID "id": "string", "type": 1 } } } ``` ### Job Specific Data For job specific data please refer to [job specific data](/works/job-result-payloads) ### Job Status Codes | Code | Status | Description | |------|--------|-------------| | `0` | **Success** | Job completed successfully | | `1` | **Generic Error** | Job failed due to generic error | | `2` | **Operation In Progress** | Another operation is currently running on the device | ### Supported Job Types The following job types will trigger Job Complete Event webhooks: | Job Type ID | Operation Name | Description | |-------------|----------------|-------------| | `1` | **Lock** | Engage the lock mechanism | | `2` | **Unlock** | Disengage the lock mechanism | | `4` | **Create PIN** | Add a new custom PIN code to the device | | `5` | **Delete PIN** | Remove an existing custom PIN code | | `9` | **Get Battery Level** | Retrieve current battery percentage | | `10` | **Get Status** | Fetch current lock state and diagnostics | | `15` | **Get Logs** | Get device activity logs | ## Bridge Connection Event Sent when a Bridge (EB1X or EB2X) connects to or disconnects from the server. This webhook indicates the bridge's current online state and can be used to update device status, trigger alerts, or drive automation when connectivity changes. ### Expected Payload ```json { "payload": { "event": { // ID for this webhook event "id": "string", // Always 10 for Bridge Connection events "type": 10, "data": { // true = bridge is online (connected); false = bridge is offline (disconnected) "isOnline": true }, // ISO 8601 timestamp of the connectivity state change "date": "ISODATE", "proxyTransportMethod": 2 }, "product": { // The bridge device ID "id": "string", "type": 1 } } } ``` ### FILE: works/iglookey-events Title: iglooKey Events Category: Webhooks ---------------------------------------- # iglooKey Events iglooworks sends webhook notifications when a recipient claims a token and when a recipient unlocks a lock with it. All webhook requests are signed — see [Webhook Security](/works/webhook-security) for how to validate them. ## Delivery Each event is sent as an HTTP `POST` to your registered webhook URL with `Content-Type: application/json` and an `x-igloocompany-sha256` signature header. Events are sent only to the iglooworks account that created the iglooKey access. ## Event Types | Event Type | Number Mapping | | --- | --- | | iglooKey Claimed | 1000 | | iglooKey Used | 1001 | Every event shares these fields: | Field | Type | Description | | --- | --- | --- | | id | string | Unique event ID (UUID) | | type | integer | Event type number | | data | object | Event-specific fields | | date | string | ISO 8601. Equals `claimedAt` for iglooKey Claimed events and `unlockedAt` for iglooKey Used events. | | proxyTransportMethod | integer | Always `1` | All timestamps are ISO 8601 strings in UTC with milliseconds (e.g. `2025-09-15T10:30:45.000Z`). ## iglooKey Claimed Event Sent once, when a recipient first claims an iglooKey token. It is not sent when: - The recipient claims a token that is already claimed. - The recipient claims an expired token. - The recipient claims a token whose access window has ended. ```json { "payload": { "event": { "id": "3f6c2a9e-8b41-4d7a-9e15-2c7d4b8a6f01", "type": 1000, "data": { "tokenId": "lnk_abc123def456ghi789jkl012mno345", "claimedAt": "2025-09-15T10:30:45.000Z" }, "date": "2025-09-15T10:30:45.000Z", "proxyTransportMethod": 1 }, "product": { "type": 1 } } } ``` `type` is always `1000` for iglooKey Claimed events. `product` carries only `type`; it has no `id`. | Field | Type | Description | | --- | --- | --- | | tokenId | string | The iglooKey token | | claimedAt | string | When the token was claimed | ## iglooKey Used Event Sent each time a recipient unlocks a lock with an iglooKey token. Each event covers one lock. - **Online unlock:** Sent when the unlock completes while the recipient's phone is online. - **Offline unlock:** Sent when the recipient's phone is back online and reports the unlock. The event can arrive well after the unlock. This example is an offline unlock reported about 95 minutes after it happened: ```json { "payload": { "event": { "id": "9a4e7c12-5d3b-4f86-b2a0-71e8c6d93f45", "type": 1001, "data": { "tokenId": "lnk_abc123def456ghi789jkl012mno345", "bluetoothId": "IGP1092b1e66", "unlockedAt": "2025-09-16T08:12:03.000Z", "reportedAt": "2025-09-16T09:47:20.000Z" }, "date": "2025-09-16T08:12:03.000Z", "proxyTransportMethod": 1 }, "product": { "id": "IGP1092b1e66", "type": 1 } } } ``` `type` is always `1001` for iglooKey Used events. `product.id` is the Bluetooth ID of the unlocked device. | Field | Type | Description | | --- | --- | --- | | tokenId | string | The iglooKey token | | bluetoothId | string | Bluetooth ID of the unlocked device | | unlockedAt | string | When the unlock happened | | reportedAt | string | When the server received the unlock | ## Handling Events - **Ordering:** Events are not guaranteed to arrive in order. - **Offline delay:** For offline unlocks, `reportedAt` can be much later than `unlockedAt`. Use `unlockedAt` for when access happened. `date` equals `unlockedAt`, so it does not show when the event was sent. ### FILE: works/job-result-payloads Title: Job Result Payloads Category: Webhooks ---------------------------------------- # Job Result Payloads This document provides detailed information about the structure and meaning of webhook payloads sent in response to various job requests. Each payload includes metadata about the event and the product involved, along with job-specific data such as status codes, battery levels, or device states. ## Overview Webhooks are used to notify your system of the outcome of asynchronous jobs initiated via API. Each webhook payload contains an `event` object that describes the job result and a `product` object identifying the device associated with the job. All timestamps are in ISO 8601 format (e.g., `2023-04-05T12:34:56Z`), `proxyTransportMethod` is `2` (sent through a Bridge), and `proxyId` is the ID of the Bridge that relayed the job. ## Common Job Status Codes The following status codes are shared across all job types unless otherwise specified: | Job Status | Number Mapping | | -------------------------------- | -------------- | | Success | `0` | | Generic failure | `1` | | Another operation is in progress | `2` | ## Lock Device ### Description This job locks the device. The webhook payload indicates whether the operation was successful. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ## Unlock Device ### Description This job unlocks the device. The webhook payload reflects the result of the unlock operation. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ## Create Custom PIN ### Description This job creates a custom PIN on the device. The webhook payload provides feedback on the creation attempt. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Status Codes | Job Status | Number Mapping | | -------------------------------- | -------------- | | Invalid Custom PIN length | `5` | | Duplicate PIN | `9` | | Insufficient PIN Storage | `12` | ## Delete PIN ### Description This job deletes a PIN from the device. The webhook payload indicates whether the deletion was successful or if the PIN was not found. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Status Codes | Job Status | Number Mapping | | -------------------------------- | -------------- | | PIN not found | `3` | ## Get Battery Level ### Description This job retrieves the current battery level of the device. The webhook payload includes both the job status and the battery level as a percentage. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "batteryLevel": 75 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Job Data | Device Battery Level Range | Description | | -------------------------- | ---------------------------------------- | | 0 - 100 | Represents battery level as a percentage | ## Get Device Status ### Description This job retrieves the current status of the device, including whether the lock and door are open. The webhook payload includes the job status and the device state. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "lockOpen": false, "doorOpen": true }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### Lock and Door States | Lock Open | Boolean Mapping | | -------------------------- | --------------- | | Locking Mechanism Not Open | `false` | | Locking Mechanism Open | `true` | | Door Open | Boolean Mapping | | ----------- | --------------- | | Door Closed | `false` | | Door Open | `true` | ## Get Activity Logs ### Description A Get Activity Logs job (`jobType` 15) produces two separate webhooks: - A Job Complete event (`type: 3`). Its `data` carries `jobId`, `jobStatus`, the raw log blob in `data`, and `hasMore`. It does not contain decoded log entries. - An [Activity Log Received event](/works/bridge-events#activity-log-received-event) (`type: 5`) with the decoded entries in `activityLogs`. It is sent only when the lock reported new log entries. The two events can arrive in either order. ### Expected Payload ```json { "payload": { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "data": "string", "hasMore": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "proxyId": "string" }, "product": { "id": "string", "type": 1 } } } ``` ### FILE: works/webhook-security Title: Webhook Security Category: Webhooks ---------------------------------------- # Webhook Security Each webhook request carries its own signature in the header. Requests are signed with an RSA key pair unique to your account, created when you add your first webhook URL. Validate the signature with your account's public key to confirm the request is authentic. Your server must accept `POST` requests — all webhook notifications are sent via `POST`. ## Setting Up Your Webhook URL Add your webhook URL on the [iglooworks dashboard](https://web.igloohome.co/works), in the **Webhooks** section. ## Signature Process Part of the webhook request is passed through HMAC, and the resulting digest is signed with your account's RSA private key. ## Webhook Request Validation Given the following HTTPS request: ``` POST /example/path HTTP/1.1 Host: example-host.com Date: Sat, 20 Jun 2015 12:34:56 GMT Accept: application/json, text/plain, */* Content-Type: application/json Content-Length: 29 User-Agent: iglooworks x-igloocompany-sha256: QA...== {"payload":"example payload"} ``` The webhook request signature is carried in the `x-igloocompany-sha256` header, base64-encoded. The body above is shortened. A real body is a JSON event wrapped in `payload`, as shown on [Bridge Events](/works/bridge-events) and [iglooKey Events](/works/iglookey-events). ### Signed Data Construction Construct the signed data from the following fields, extracted from the request, in order: - Method (all uppercase) - Host (hostname only, without port) - URL Path (without query string) - Content Type (always `application/json`) - Date - Body Host and URL Path come from your registered webhook URL, which is stored in lowercase. Concatenate each item, delimited by the bar (`|`) character. For the request above, the final concatenated payload is: ``` POST|example-host.com|/example/path|application/json|Sat, 20 Jun 2015 12:34:56 GMT|{"payload":"example payload"} ``` ### HMAC Process Hash the constructed signed data using HMAC with the SHA-256 hashing algorithm to get the HMAC digest. Use the decoded public key (see [Public Key](#public-key)) as the HMAC key. ### Signature Validation Decode the signature from the header and verify it against the HMAC digest with your public key. The signature is RSA PKCS#1 v1.5 with SHA-256. ### Public Key | Property | Value | | --- | --- | | Length | 2048 | | Cipher | RSA | | Format | DER | | Type | PKCS1 | Contact `dev+support@igloohome.co` to request your account's public key. The key you receive is base64-encoded. ## Working Example The following Node.js example (`crypto` library, v16.x LTS) validates the signature of an example payload with a sample public key: ```javascript // Using nodejs crypto library. import * as crypto from 'crypto'; // The example payload that we'll be validating. const payload = 'POST|example-host.com|/example/path|application/json|Sat, 20 Jun 2015 12:34:56 GMT|{"payload":"example payload"}'; // We use SHA-256 hash algorithm in both the HMAC computation and signature validation. const hashAlgorithm = 'sha256'; // Decode both the public key string and signature string. // (Assuming `publicKeyString` and `signatureString` are both valid.) const publicKey = Buffer.from(publicKeyString, 'base64'); const signature = Buffer.from(signatureString, 'base64'); // Compute the hash using HMAC. The body is hashed as UTF-8 bytes. const hmacDigest = crypto.createHmac(hashAlgorithm, publicKey) .update(payload, 'utf8') .digest(); // Validate signature. const result = crypto.verify( hashAlgorithm, hmacDigest, crypto.createPublicKey({ key: publicKey, format: 'der', type: 'pkcs1' }), signature ); // Output validation result. console.log('signature valid:', result); // signature valid: true ```