# igloo Platform Monolithic Context Corpus > This file contains the entire documentation suite inlined for AI agents and LLMs. ## API Reference ### FILE: home/api_algopin Title: Algopin Category: API Reference ---------------------------------------- # Algopin API Reference Enterprise API endpoints for managing algopin. ## Create a duration (daily) algoPIN code `POST` `/devices/{deviceId}/algopin/daily` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/daily" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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 a duration (hourly) algoPIN code `POST` `/devices/{deviceId}/algopin/hourly` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/hourly" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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 code `POST` `/devices/{deviceId}/algopin/onetime` Returns a one-time algoPIN code (OTP). ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/onetime" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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 duration passage algoPIN code. `POST` `/devices/{deviceId}/algopin/passage` Allows unlock with AlgoPIN and keeps the lock unlocked for the duration provided in the request body. Only supports newer devices with latest firmware. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | durationHours | integer | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/passage" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` `418 '{ error: 'this device type does not support the feature.' }' — the device SKU does not support algoPIN codes.` `500 Internal Server Error` **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 a permanent algoPIN code `POST` `/devices/{deviceId}/algopin/permanent` Returns a permanent algoPIN code. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/algopin/permanent" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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." } ``` ### FILE: home/api_bridge_jobs Title: Bridge Jobs Category: API Reference ---------------------------------------- # Bridge Jobs API Reference Enterprise API endpoints for managing bridge jobs. ## Create a Bridge Targeted Job `POST` `/devices/{bridgeId}/jobs` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | bridgeId | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | jobType | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{bridgeId}/jobs" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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.' }'. A 'Retry-After' header (seconds) is included.` `500 Internal Server Error` **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` `/devices/{deviceId}/jobs/bridges/{bridgeId}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | | bridgeId | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | jobData | string | Optional | | jobType | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/jobs/bridges/{bridgeId}" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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` `/jobs/{jobId}` Returns the status of a previously created job. ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 MongoDB ObjectId` `500 Internal Server Error` **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_devices Title: Devices Category: API Reference ---------------------------------------- # Devices API Reference Enterprise API endpoints for managing devices. ## Get Devices `GET` `/devices` ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | | type | query | No | | search | query | No | | expand | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices?limit=value&cursor=value&sort=value&type=value&search=value&expand=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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&type=value&search=value&expand=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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&type=value&search=value&expand=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_API_KEY", "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_API_KEY", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value", "type": "value", "search": "value", "expand": "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&type=value&search=value&expand=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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&type=value&search=value&expand=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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&type=value&search=value&expand=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_API_KEY", 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` **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` `/devices/support-pins` ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | | type | query | No | | search | query | No | | expand | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/support-pins?limit=value&cursor=value&sort=value&type=value&search=value&expand=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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&type=value&search=value&expand=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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&type=value&search=value&expand=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_API_KEY", "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_API_KEY", "Accept": "application/json" } params = { "limit": "value", "cursor": "value", "sort": "value", "type": "value", "search": "value", "expand": "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&type=value&search=value&expand=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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&type=value&search=value&expand=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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&type=value&search=value&expand=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_API_KEY", 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](#/paths/~1devices/get).` `401 Unauthorized, invalid or expired authorization token` `402 Response` `403 Forbidden, accessToken scope must contain 'igloohomeapi/get-devices'` `500 Internal Server Error` **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` `/devices/{deviceId}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | admin_key | query | No | | lock_id | query | No | | expand | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/devices/{deviceId}?admin_key=value&lock_id=value&expand=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" ``` ```go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/devices/{deviceId}?admin_key=value&lock_id=value&expand=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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}?admin_key=value&lock_id=value&expand=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_API_KEY", "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_API_KEY", "Accept": "application/json" } params = { "admin_key": "value", "lock_id": "value", "expand": "value" } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```php "https://api.igloohome.co/home/devices/{deviceId}?admin_key=value&lock_id=value&expand=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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}?admin_key=value&lock_id=value&expand=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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}?admin_key=value&lock_id=value&expand=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_API_KEY", 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` **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` `/devices/{deviceId}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | batteryLevel | string | Optional | ### Request Example ```bash curl -X PUT "https://api.igloohome.co/home/devices/{deviceId}" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 Successfully` `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` **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` `/devices/{deviceId}/activity` Returns a paginated list of activity logs recorded for a device. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | startDate | query | No | | endDate | query | No | | locale | query | No | | limit | query | No | | cursor | query | No | | sort | query | No | ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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` `/devices/{deviceId}/activity` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | logsPayload | array | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/activity" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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` `/devices/{deviceId}/masterpin` ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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_ekey Title: Ekey Category: API Reference ---------------------------------------- # Ekey API Reference Enterprise API endpoints for managing ekey. ## Generate Bluetooth GuestKey `POST` `/devices/{deviceId}/ekey` Create a bluetooth guestkey. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | endDate | string | Optional | | permissions | string | Optional | | startDate | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/devices/{deviceId}/ekey" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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" ``` ### FILE: home/api_iglookey Title: Iglookey Category: API Reference ---------------------------------------- # Iglookey API Reference Enterprise API endpoints for managing iglookey. ## Create iglooKey Token (Smart Link) `POST` `/pass` ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | activationWindowSeconds | string | Optional | | deviceIds | string | Optional | | endsAt | string | Optional | | label | string | Optional | | offlineAccessSeconds | string | Optional | | startsAt | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/pass" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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" } ``` ## Create iglooKey Token (Offline capable) `POST` `/pass/offline` Same as [Create iglooKey Token](#/paths/~1pass/post); offline access is supported. ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | activationWindowSeconds | string | Optional | | deviceIds | string | Optional | | endsAt | string | Optional | | label | string | Optional | | offlineAccessSeconds | string | Optional | | startsAt | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/pass/offline" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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/offline" 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_API_KEY") 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/offline", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "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/offline" headers = { "Authorization": "Bearer YOUR_API_KEY", "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/offline", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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/offline") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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/offline")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_API_KEY", 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` **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" } ``` ## Create iglooKey Token (Online only) `POST` `/pass/online` Same as [Create iglooKey Token](#/paths/~1pass/post) but the resulting token does not support offline access; `offlineAccessSeconds` is rejected. ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | activationWindowSeconds | string | Optional | | deviceIds | string | Optional | | endsAt | string | Optional | | label | string | Optional | | startsAt | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/pass/online" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "startsAt": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/pass/online" payload := []byte(`{ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "startsAt": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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/online", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "startsAt": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/pass/online" headers = { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json" } payload = { "activationWindowSeconds": "value", "deviceIds": "value", "endsAt": "value", "label": "value", "startsAt": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/pass/online", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "activationWindowSeconds" => "value", "deviceIds" => "value", "endsAt" => "value", "label" => "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", "startsAt": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/pass/online") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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/online")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_API_KEY", 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", "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` **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` `/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` | Parameter | Type | Required | | --- | --- | --- | | endsAt | string | Optional | | startsAt | string | Optional | ### Request Example ```bash curl -X PATCH "https://api.igloohome.co/home/pass/{id}" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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.` **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` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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.` **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_properties Title: Properties Category: API Reference ---------------------------------------- # Properties API Reference Enterprise API endpoints for managing properties. ## Get Properties `GET` `/properties` Returns list of properties on the account ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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` `/properties/{propertyId}/devices` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | propertyId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | cursor | query | No | | limit | query | No | | sort | query | No | | type | query | No | | search | query | No | | expand | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/properties/{propertyId}/devices?cursor=value&limit=value&sort=value&type=value&search=value&expand=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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&type=value&search=value&expand=value" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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&type=value&search=value&expand=value", { method: "GET", headers: { "Authorization": "Bearer YOUR_API_KEY", "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_API_KEY", "Accept": "application/json" } params = { "cursor": "value", "limit": "value", "sort": "value", "type": "value", "search": "value", "expand": "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&type=value&search=value&expand=value", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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&type=value&search=value&expand=value") .method("GET", null) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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&type=value&search=value&expand=value")!) request.httpMethod = "GET" request.setValue("Bearer YOUR_API_KEY", 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` **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 Enterprise API endpoints for managing server health. ## Get Server / OAuth Client Health `GET` `/server-health` ### Request Example ```bash curl -X GET "https://api.igloohome.co/home/server-health" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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` **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: home/api_token Title: Token Category: API Reference ---------------------------------------- # Token API Reference Enterprise API endpoints for managing token. ## Get or Refresh Access Token `POST` `/token` ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | clientId | string | Optional | | clientSecret | string | Optional | | code | string | Optional | | grantType | string | Optional | | redirectUri | string | Optional | | refreshToken | string | Optional | | scope | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/token" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/token" payload := []byte(`{ "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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/token", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/token" headers = { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json" } payload = { "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/token", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "clientId" => "value", "clientSecret" => "value", "code" => "value", "grantType" => "value", "redirectUri" => "value", "refreshToken" => "value", "scope" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/token") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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/token")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "clientId": "value", "clientSecret": "value", "code": "value", "grantType": "value", "redirectUri": "value", "refreshToken": "value", "scope": "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` `500 Internal Server Error. Also returned for any Cognito-rejected grant whose error name is outside the 'invalid_request'/'invalid_client'/'invalid_grant' whitelist above (e.g. 'unauthorized_client').` **Example response — `200`:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 3600, "refresh_token": "refresh_token_def789ghi012jkl345", "token_type": "Bearer" } ``` **Example response — `400`:** ```json { "error": "invalid_client" } ``` ## Revoke Refresh Token `POST` `/token/revoke` Revokes a previously issued refresh token via Cognito. Unauthenticated. ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | clientId | string | Optional | | clientSecret | string | Optional | | refreshToken | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/home/token/revoke" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "clientId": "value", "clientSecret": "value", "refreshToken": "value" }' ``` ```go package main import ( "bytes" "fmt" "io" "net/http" ) func main() { url := "https://api.igloohome.co/home/token/revoke" payload := []byte(`{ "clientId": "value", "clientSecret": "value", "refreshToken": "value" }`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer YOUR_API_KEY") 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/token/revoke", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ "clientId": "value", "clientSecret": "value", "refreshToken": "value" }) }); const data = await res.json(); console.log(data); ``` ```python import requests url = "https://api.igloohome.co/home/token/revoke" headers = { "Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json" } payload = { "clientId": "value", "clientSecret": "value", "refreshToken": "value" } response = requests.post(url, headers=headers, json=payload) print(response.json()) ``` ```php "https://api.igloohome.co/home/token/revoke", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "Accept: application/json", "Content-Type: application/json" ], CURLOPT_POSTFIELDS => json_encode([ "clientId" => "value", "clientSecret" => "value", "refreshToken" => "value" ]) ]); $response = curl_exec($curl); curl_close($curl); echo $response; ``` ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = """ { "clientId": "value", "clientSecret": "value", "refreshToken": "value" } """.trimIndent().toRequestBody(mediaType) val request = Request.Builder() .url("https://api.igloohome.co/home/token/revoke") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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/token/revoke")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let payload: [String: Any] = [ "clientId": "value", "clientSecret": "value", "refreshToken": "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` `500 Internal Server Error. Also returned for any other Cognito-rejected error name outside the 'invalid_request'/'invalid_client' whitelist.` **Example response — `400`:** ```json { "error": "invalid_client" } ``` ### FILE: works/api_access Title: Access Category: API Reference ---------------------------------------- # Access API Reference Enterprise API endpoints for managing access. ## Create a duration passage algoPIN code. `POST` `/devices/{deviceId}/algopin/passage` Allows unlock with AlgoPIN and keeps the lock unlocked for the duration provided in the request body. Only supports newer devices with latest firmware. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | deviceId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | Authorization | header | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | durationHours | integer | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{deviceId}/algopin/passage" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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/works/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_API_KEY") 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}/algopin/passage", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "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/works/devices/{deviceId}/algopin/passage" headers = { "Authorization": "Bearer YOUR_API_KEY", "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/works/devices/{deviceId}/algopin/passage", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => [ "Authorization: Bearer YOUR_API_KEY", "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/works/devices/{deviceId}/algopin/passage") .method("POST", body) .addHeader("Authorization", "Bearer YOUR_API_KEY") .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}/algopin/passage")!) request.httpMethod = "POST" request.setValue("Bearer YOUR_API_KEY", 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. If a matching PIN already exists for this lock, type, and window with the same 'variance', this returns the same success response without creating a new PIN. This is expected, idempotent behavior, 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 the broad algopin scope, OR missing the specific scope for this exact PIN type (double-gated — see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **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" } ``` ## List access records for a device `GET` `/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 | Parameter | In | Required | | --- | --- | --- | | accessType | query | No | | limit | query | No | | cursor | query | No | | sort | query | No | ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Device not found, or (Lock/Keypad only) Property not found.` `409 Property timezone is missing.` **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" } ``` ## Grant ekey access (persists Access + Activity + Audit trail) `POST` `/devices/{id}/access/grant` 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` | Parameter | Type | Required | | --- | --- | --- | | departmentRef | string | Optional | | endDateTime | string | Optional | | lang | string | Optional | | permissions | string | Optional | | recipientId | string | Optional | | startDateTime | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/access/grant" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 access record is saved, this still returns 201, but with a different response body (no 'code' field) containing an error message instead.` `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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `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.` **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(es) for a recipient on this device `POST` `/devices/{id}/access/revoke` Revokes ALL matching access records for this recipient and permanently deletes their associated key material. This action is permanent and cannot be undone. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | recipientId | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/access/revoke" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `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, there is no tolerant fallback if activity/audit-trail logging fails; any failure here is a hard 500.` **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" } ``` ## Delete/revoke a PIN access (creates an async delete job) `DELETE` `/devices/{id}/access/{accessId}` ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Access not found ('{error: "access not found"}').` `409 Response` `415 Missing 'Content-Type: application/json' — enforced even though this DELETE has no meaningful body fields.` **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" } ``` ## Create a Daily Algo PIN `POST` `/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–5). ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | departmentId | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/daily" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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', this returns the same success response without creating a new PIN. This is expected, idempotent behavior, 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 the broad algopin scope, OR missing the specific scope for this exact PIN type (double-gated — see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **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 Algo PIN `POST` `/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–5). ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | departmentId | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/hourly" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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', this returns the same success response without creating a new PIN. This is expected, idempotent behavior, 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 the broad algopin scope, OR missing the specific scope for this exact PIN type (double-gated — see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **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 Algo PIN `POST` `/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` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | departmentId | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/onetime" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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', this returns the same success response without creating a new PIN. This is expected, idempotent behavior, 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 the broad algopin scope, OR missing the specific scope for this exact PIN type (double-gated — see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **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 Algo PIN `POST` `/devices/{id}/algopin/permanent` Request and response shape matches the other Algo PIN endpoints (onetime/daily/hourly) — see `AlgoPinRequest`. Requires the `algopin-permanent` scope specifically; holding a different Algo PIN 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` | Parameter | Type | Required | | --- | --- | --- | | accessName | string | Optional | | departmentId | string | Optional | | endDate | string | Optional | | startDate | string | Optional | | variance | integer | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/algopin/permanent" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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', this returns the same success response without creating a new PIN. This is expected, idempotent behavior, 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 the broad algopin scope, OR missing the specific scope for this exact PIN type (double-gated — see the endpoint description).` `409 Response` `415 Missing 'Content-Type: application/json'.` **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" } ``` ## Generate a Bluetooth guest ekey (no DB persistence) `POST` `/devices/{id}/ekey` Error responses are returned as plain text rather than JSON. Only a successful response returns a JSON body. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | endDate | string | Optional | | permissions | string | Optional | | startDate | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/ekey" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `409 Plain-text body: '"failed.generate.ekey"' (key derivation failed).` `415 Missing 'Content-Type: application/json'.` `500 Plain-text body — uncaught exception.` **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" } ``` ### FILE: works/api_account Title: Account Category: API Reference ---------------------------------------- # Account API Reference Enterprise API endpoints for managing account. ## Get this account's details `GET` `/account` ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/account" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 Enterprise API endpoints for managing activity. ## List (translated) activity logs for a device `GET` `/devices/{id}/activity` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | startDate | query | No | | endDate | query | No | | locale | query | No | | limit | query | No | | cursor | query | No | | sort | query | No | ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 raw device activity logs `POST` `/devices/{id}/activity` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | logsPayload | array | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/activity" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `415 Missing 'Content-Type: application/json'.` **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_bridges Title: Bridges Category: API Reference ---------------------------------------- # Bridges API Reference Enterprise API endpoints for managing bridges. ## Get a bridge-proxied job's status/result `GET` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Job not found.` `422 jobId is not a valid ID.` **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" } ``` ### FILE: works/api_departments Title: Departments Category: API Reference ---------------------------------------- # Departments API Reference Enterprise API endpoints for managing departments. ## List departments on this account `GET` `/departments` ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 properties in a department `GET` `/departments/{departmentId}/properties` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | departmentId | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{departmentId}/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `409 Department not found.` **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 access records for a department `GET` `/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 | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{id}/access?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Department not found.` **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 devices in a department `GET` `/departments/{id}/devices` Same response shape as GET /devices, scoped to the department. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/departments/{id}/devices?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Department not found.` **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 Enterprise API endpoints for managing devices. ## List devices on this account `GET` `/devices` ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | type | query | No | | search | query | No | | expand | query | No | | limit | query | No | | cursor | query | No | | sort | query | No | ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 a single device `GET` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Device not found.` **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 a G2 device's obfuscated admin key `GET` `/devices/{id}/admin-key` Only supported for G2 devices — G3 devices keep their admin key encrypted server-side and are not readable via this route. The response is an obfuscated blob, uniquely keyed per device, that requires client-side decryption. ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Device not found.` `500 '{message: "internal.server.error"}' — uncaught exception.` **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 Enterprise API endpoints for managing jobs. ## Create a bridge-proxied job (lock/unlock/PIN/status/logs via a Bridge) `POST` `/devices/{deviceId}/jobs/bridges/{bridgeId}` `jobData`'s shape 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` | Parameter | Type | Required | | --- | --- | --- | | departmentId | string | Optional | | jobData | string | Optional | | jobType | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{deviceId}/jobs/bridges/{bridgeId}" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `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 flat org).` `415 Missing 'Content-Type: application/json'.` **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" } ``` ## List jobs for a device `GET` `/devices/{id}/jobs` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | jobType | query | No | | status | query | No | | departmentId | query | No | | limit | query | No | | cursor | query | No | | sort | query | No | ### 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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 a custom Bluetooth PIN (async job) `POST` `/devices/{id}/jobs` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | customPin | string | Optional | | departmentId | string | Optional | | description | string | Optional | | endDateTime | string | Optional | | pinType | string | Optional | | startDateTime | string | Optional | ### Request Example ```bash curl -X POST "https://api.igloohome.co/works/devices/{id}/jobs" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `409 Response` `415 Missing 'Content-Type: application/json'.` **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 a single job's details `GET` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Job not found.` **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 a job's status (called by device/bridge sync flows) `PATCH` `/devices/{id}/jobs/{jobId}` ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | | jobId | string | Yes | ### Body Parameters Content-Type: `application/json` | Parameter | Type | Required | | --- | --- | --- | | reason | string | Optional | | status | string | Optional | ### Request Example ```bash curl -X PATCH "https://api.igloohome.co/works/devices/{id}/jobs/{jobId}" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Job not found.` **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 Enterprise API endpoints for managing properties. ## List properties on this account `GET` `/properties` ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/properties?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` **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 a single property `GET` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Property not found.` **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 devices in a property `GET` `/properties/{id}/devices` Same response shape as GET /devices, scoped to the property. ### Path Parameters | Parameter | Type | Required | | --- | --- | --- | | id | string | Yes | ### Query Parameters | Parameter | In | Required | | --- | --- | --- | | limit | query | No | | cursor | query | No | | sort | query | No | ### Request Example ```bash curl -X GET "https://api.igloohome.co/works/properties/{id}/devices?limit=value&cursor=value&sort=value" \ -H "Authorization: Bearer YOUR_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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 OAuth client's access has been revoked ('{error: "The user has revoked access for this application..."}'). Never returned on the API-key auth path, which does not check scope at all.` `404 Property not found.` **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 Enterprise API endpoints for managing users. ## List users on this account `GET` `/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_API_KEY" \ -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_API_KEY") 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_API_KEY", "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_API_KEY", "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_API_KEY", "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_API_KEY") .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_API_KEY", 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"}'.` **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" } ``` ## Articles on Bridge API ### FILE: home/bridge-jobs-proxied Title: Jobs for Lock Category: Articles on Bridge API ---------------------------------------- # Jobs for Lock (Bridge Proxied) 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. For endpoint request/response signatures, see the [Bridge Jobs API Reference](/home/api_bridge_jobs). ## 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 | Supported | | --- | --- | --- | | Lock | 1 | Yes | | Unlock | 2 | Yes | | Edit Custom PIN code | 3 | No | | Create Custom PIN code | 4 | Yes | | Delete PIN code | 5 | Yes | | Get Battery Level | 9 | Yes | | Get Device Status | 10 | Yes | | Set Maximum Incorrect PIN code Attempts | 14 | No | | Get Activity Logs | 15 | Yes | | Set Master PIN code | 16 | No | | Revoke Bluetooth Guest Key | 18 | No | | Re-enable revoked Bluetooth Guest Key | 19 | No | | Delete RFID Card by UID | 24 | No | | Delete Fingerprint by UID | 29 | No | ## 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 | 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) | No | **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. Unactivated algoPINs can only be deleted within one week of their start time. | Parameter | Type | Description | Required | | --- | --- | --- | --- | | pin | string | PIN code to be deleted | 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 | `` is required | All | | 400 | `` is invalid | All | | 400 | `` can only contain digits 0 to 9 | Custom PIN code Commands | | 400 | `` length must be 4 to 6 digits long | Custom PIN code Commands | | 400 | `` must be in format YYYY-MM-DDTHH:mm:ss+hh:mm | Custom PIN code Commands | | 400 | `` must be now or after | Custom PIN code Commands | | 400 | `` must be after 'startDate' | Custom Duration PIN code Command | | 401 | No valid API key and/or access token provided | All | | 404 | The resource was not found | All | | 405 | Method not allowed | All | | 406 | Bridge is offline | All | | 415 | Unsupported media type | N/A | ### FILE: home/bridge-jobs-targeted Title: Jobs for Bridge Category: Articles on Bridge API ---------------------------------------- # Jobs for Bridge (Bridge Targeted) These jobs only target the Bridge device itself, and do not target any lock device. For endpoint request/response signatures, see the [Bridge Jobs API Reference](/home/api_bridge_jobs). ## Job Creation Create a job by making a `POST` request to: `/devices/{deviceId}/jobs` | 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. ## 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. Maximum 1 request every 5 minutes. | All | | 500 | Internal Server Error | An unexpected error occurred on our end. Please retry later or contact support. | All | ### FILE: works/bridge-overview Title: Overview Category: Articles on Bridge API ---------------------------------------- # Bridge API Overview Bridge devices act as an intermediary between the internet and other devices, allowing for remote access. This can be used for proxied jobs - which target devices through the Bridge, or for jobs which target the Bridge itself. In addition, Bridge devices support Webhook event notifications for both types of jobs and automated heartbeat events. * [Bridge Proxied Jobs](https://igloohome.stoplight.io/docs/iglooworks/7507518fdda1p-create-a-bridge-proxied-job) ## Retrieving Job Status You may also wish to check the status of the jobs that were sent. This can be done by referencing the API [here](https://igloohome.stoplight.io/docs/iglooworks/ok37xn58wr52p-get-bridge-job-details) ### FILE: works/bridge-proxied-jobs Title: Remote Job for Locks Category: Articles on Bridge API ---------------------------------------- # Bridge Proxied Jobs Bridge Proxied Jobs enable you to send commands to smart locks through a Bridge device. This acts as an intermediary that communicates with locks via Bluetooth while your application communicates with the Bridge via the API. ## Contents - [Job Creation](#job-creation) - [Command Details](#command-details) - [Lock](#lock) - [Unlock](#unlock) - [Create Custom PIN code](#create-custom-pin-code) - [Delete PIN code](#delete-pin-code) - [Get Battery Level](#get-battery-level) - [Get Device Status](#get-device-status) - [Get Activity Logs](#get-activity-logs) - [Error Status Codes](#error-status-codes) ## 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 | ### Response ```json { "jobId": "string" } ``` ### Job Types | Job Type | ID | Supported | | ----------------------------------------- | ---- | ----------- | | Lock | 1 | Yes | | Unlock | 2 | Yes | | Edit Custom PIN code | 3 | No | | Create Custom PIN code | 4 | Yes | | Delete PIN code | 5 | Yes | | Get Battery Level | 9 | Yes | | Get Device Status | 10 | Yes | | Set Maximum Incorrect PIN code Attempts | 14 | No | | Get Activity Logs | 15 | Yes | | Set Master PIN code | 16 | No | | Revoke Bluetooth Guest Key | 18 | No | | Re-enable revoked Bluetooth Guest Key | 19 | No | | Delete RFID Card by UID | 24 | No | | Delete Fingerprint by UID | 29 | No | ## 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 | 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) | No | **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** ```OTP title="One-Time PIN (24-hour validity):" { "jobType": 4, "jobData": { "accessName": "OTP PIN", "pin": "123456", "pinType": 1, "startDate": "2024-01-01T00:00:00+00:00" } } ``` ```Permanent title="Permanent PIN:" { "jobType": 4, "jobData": { "accessName": "Permanent PIN", "pin": "123456", "pinType": 2, "startDate": "2024-01-01T00:00:00+00:00" } } ``` ```Duration title="Duration PIN (specific time window):" { "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 code from the lock. > **Note**: Unactivated algoPINs can only be deleted within one week of their start time. **Parameters** | Parameter | Type | Description | Required | | ----------- | -------- | ------------------------ | ---------- | | pin | string | PIN code to be deleted | Yes | **Request Example** ```json { "jobType": 5, "jobData": { "pin": "123456" } } ``` ### 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 | `` is required | All | | 400 | `` is invalid | All | | 400 | `` can only contain digits 0 to 9 | Custom PIN code Commands | | 400 | `` length must be 4 to 6 digits long | Custom PIN code Commands | | 400 | `` must be in format YYYY-MM-DDTHH:mm:ss+hh:mm | Custom PIN code Commands | | 400 | `` must be now or after | Custom PIN code Commands | | 400 | `` must be after 'startDate' | Custom Duration PIN code Command | | 401 | No valid API key and/or access token provided | All | | 404 | The resource was not found | All | | 405 | Method not allowed | All | | 406 | Bridge is offline | All | | 415 | Unsupported media type | N/A | ## Articles on Webhook ### FILE: home/webhook-job-specific-data Title: Job Specific Data Category: Articles on Webhook ---------------------------------------- # Job-Specific Webhook Data 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`), and `proxyTransportMethod` is consistently set to `2`, indicating the transport mechanism used for communication. ## 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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "batteryLevel": 75 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "lockOpen": false, "doorOpen": true }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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` | ### FILE: home/webhook-security Title: Webhook Security Category: Articles on Webhook ---------------------------------------- # Webhook Security Each webhook request sent carries its own signature in the header. A public key obtained from the portal or our support team can be used to validate the signature and ensure the authenticity of the request. 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/api-access) portal, in the **Webhooks** section. ### iglooconnect Partners Email `dev+support@igloocompany.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, where the resulting digest is signed using our 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 Content-Type: application/json Content-Length: 29 x-igloocompany-sha256: "QA...==" { "payload": "example payload" } ``` The webhook request signature is carried in the `x-igloocompany-sha256` header, base64-encoded. ### Signed Data Construction Construct the signed data from the following fields, extracted from the request, in order: - Method (all uppercase) - Host - URL Path - Content Type - Date - Body 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"} ``` ### HMAC Process Hash the constructed signed data using HMAC with the SHA-256 hashing algorithm to get the HMAC digest. ### Signature Validation Validate the HMAC digest against the signature extracted from the header and decoded, using SHA-256. ### Public Key | Property | Value | | --- | --- | | Length | 2048 | | Cipher | RSA | | Format | DER | | Type | PKCS1 | Contact `dev+support@igloocompany.co` to request the 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. const hmacDigest = crypto.createHmac(hashAlgorithm, publicKey) .update(payload, 'ascii') .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: home/webhooks-bridge-event Title: Bridge Event Webhooks Category: Articles on Webhook ---------------------------------------- # Bridge Webhook 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 [Jobs for Lock (Bridge Proxied)](/home/bridge-jobs-proxied) and [Jobs for Bridge (Bridge Targeted)](/home/bridge-jobs-targeted)). 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-Specific Webhook Data](#job-specific-webhook-data) below. `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, "accessoryId": "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 Bluetooth Guest key 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 Bluetooth Guest key | value3 | Guest key id | | 40 | Unblacklist Bluetooth Guest key | value3 | Guest key 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 [Jobs for Bridge (Bridge Targeted)](/home/bridge-jobs-targeted)). | 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 | ## Job-Specific Webhook Data Each [Bridge Proxied Job](/home/bridge-jobs-proxied)'s Job Complete Event webhook (`type: 3`) carries job-specific fields in `data`, alongside the common fields (`jobId`, `jobStatus`, `date`, `proxyTransportMethod`, `accessoryId`) and a `product` object identifying the device. All timestamps are ISO 8601 (e.g. `2023-04-05T12:34:56Z`); `proxyTransportMethod` is always `2`. **Common Job Status Codes** (shared across all job types unless a job overrides them below) | Job Status | Number Mapping | | --- | --- | | Success | 0 | | Generic failure | 1 | | Another operation is in progress | 2 | ### Lock / Unlock Device No job-specific fields — `data` carries only `jobId` and `jobStatus`. ```json { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "string" }, "product": { "id": "string", "type": 1 } } ``` ### Create Custom PIN No job-specific data fields beyond `jobId`/`jobStatus`, but adds these `jobStatus` codes: | Job Status | Number Mapping | | --- | --- | | Invalid Custom PIN length | 5 | | Duplicate PIN | 9 | | Insufficient PIN Storage | 12 | ### Delete PIN Adds this `jobStatus` code: | Job Status | Number Mapping | | --- | --- | | PIN not found | 3 | ### Get Battery Level `data` adds `batteryLevel`: ```json { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "batteryLevel": 75 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "string" }, "product": { "id": "string", "type": 1 } } ``` | Device Battery Level Range | Description | | --- | --- | | 0 - 100 | Represents battery level as a percentage | ### Get Device Status `data` adds `lockOpen` and `doorOpen`: ```json { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "lockOpen": false, "doorOpen": true }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "string" }, "product": { "id": "string", "type": 1 } } ``` | Lock Open | Boolean Mapping | | --- | --- | | Locking Mechanism Not Open | false | | Locking Mechanism Open | true | | Door Open | Boolean Mapping | | --- | --- | | Door Closed | false | | Door Open | true | ### FILE: works/webhook-automated-event Title: Bridge Event Webhooks Category: Articles on Webhook ---------------------------------------- # Bridge Automated 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, accessoryId: 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 Bluetooth Guest key 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 Bluetooth Guest key | value3 | Guest key id | | 40 | Unblacklist Bluetooth Guest key | value3 | Guest key 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/webhook-job-specific-data) ### 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) 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/webhook-job-specific-data Title: Job Specific Data Category: Articles on Webhook ---------------------------------------- # Job-Specific Webhook Data 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`), and `proxyTransportMethod` is consistently set to `2`, indicating the transport mechanism used for communication. ## 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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "batteryLevel": 75 }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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 { "event": { "id": "string", "type": 3, "data": { "jobId": "string", "jobStatus": 0, "lockOpen": false, "doorOpen": true }, "date": "ISO_DATE", "proxyTransportMethod": 2, "accessoryId": "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` | ### FILE: works/webhook-security Title: Webhook Security Category: Articles on Webhook ---------------------------------------- # Webhook Security For security, each webhook request sent contain its own signature in the header. A public key obtained from the portal or our support team can be used to validate the signature to ensure the authenticity of the request. > **Important:** Make sure your server accept POST requests, as all our webhook notifications are sent via POST. ## Setting up your webhook URL ### iglooworks dashboard Add your webhook URL on our **[iglooworks dashboard](https://dashboard.iglooworks.co/)** portal in the **Webhooks** section. ## Contents - [Signature Process](#signature-process) - [Signature Validation](#signature-validation) - [Signed Data construction](#signed-data-construction) - [HMAC process](#hmac-process) - [Signature Validation](#signature-validation) - [Public Key](#public-key) - [Working Example](#working-example) ## Signature Process Parts of the webhook request is passed through HMAC, where the resulting digest is signed using our RSA private key. ## Webhook Request Validation Assuming you received the following HTTPS request: ```txt POST /example/path HTTP/1.1 Host: example-host.com Date: Sat, 20 Jun 2015 12:34:56 GMT Accept: application/json Content-Type: application/json Content-Length: 29 x-igloocompany-sha256: "QA...==" { "payload": "example payload" } ``` The webhook request signature is indicated by the header "x-igloocompany-sha256" and its contents are encoded in base64. ### Signed Data construction You first construct the signed data using the following information extracted from the request in order: 1. Method (all uppercase) 2. Host 3. URL Path 4. Content Type 5. Date 6. Body Each item must be concatenated and delimited using the bar ("`|`") character. The following example shows the final concatenated payload: ```txt 'POST|example-host.com|/example/path|application/json|Sat, 20 Jun 2015 12:34:56 GMT|{"payload":"example"}' ``` ### HMAC Process The constructed signed data can then be hashed using HMAC with the _SHA-256_ hashing algorithm, giving you the **HMAC digest**. ### Signature Validation Upon acquiring the HMAC digest of the signed data, you may proceed with validating its signature extracted from the header and decoded. The hash algorithm used in the signature validation is _SHA-256_. ### Public key Public key properties: - Length: 2048 - Cipher: RSA - Format: DER - Type: PKCS1 _Note: Contact our support **[dev+support@igloocompany.co](mailto:dev+support@igloocompany.co?subject=Public%20Key%20Request%20-%20Iglooconnect%20Partner)** for acquiring public key. Public key acquired is encoded in base64._ ## Working Example Here is a source code in _Node.js_ that performs signature validation of an example payload with a sample public key using the `crypto` library ("v16.X LTS" at the time of writing). ```js // 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. const hmacDigest = crypto.createHmac(hashAlgorithm, publicKey) .update(payload, 'ascii') .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 ``` ## BLE SDK ### FILE: home/sdk_android_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()` and `syncWithStatus()` methods require 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 sdk.syncWithStatus( deviceId = deviceId, key = guestKey, getDeviceToken = tokens.getDeviceToken, storeLogsToken = tokens.storeLogsToken, updateDeviceToken = tokens.updateDeviceToken, ).collect { status -> // Handle each sync operation result } ``` ```swift for await status in sdk.syncWithStatus( deviceId, key: guestKey, getDeviceToken: tokens.getDeviceToken, storeLogsToken: tokens.storeLogsToken, updateDeviceToken: tokens.updateDeviceToken ) { // Handle each sync operation result } ``` ### 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, accessToken) } 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, accessToken) _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. --- ## Sync with Granular Error Handling Use `syncWithStatus()` to handle errors per sync operation instead of failing the entire sync on the first error. ```kotlin sdk.syncWithStatus( deviceId = deviceId, key = guestKey, getDeviceToken = tokens.getDeviceToken, storeLogsToken = tokens.storeLogsToken, updateDeviceToken = tokens.updateDeviceToken, ).collect { status -> when (status.operation) { SyncResultStatus.Operation.SET_TIME -> if (!status.isSuccess) log("Set time failed: ${status.error?.message}") SyncResultStatus.Operation.GET_BATTERY_LEVEL -> if (status.isSuccess) updateBatteryUi() SyncResultStatus.Operation.SYNC_ACTIVITY_LOGS -> if (!status.isSuccess) log("Log sync failed: ${status.error?.message}") } } ``` ```swift for await status in sdk.syncWithStatus( deviceId, key: guestKey, getDeviceToken: tokens.getDeviceToken, storeLogsToken: tokens.storeLogsToken, updateDeviceToken: tokens.updateDeviceToken ) { switch status.operation { case .SET_TIME: if !status.isSuccess { log("Set time failed: \(status.error?.localizedDescription ?? "")") } case .GET_BATTERY_LEVEL: if status.isSuccess { updateBatteryUi() } case .SYNC_ACTIVITY_LOGS: if !status.isSuccess { log("Log sync failed: \(status.error?.localizedDescription ?? "")") } } } ``` Each operation emits a `SyncResultStatus` with its own success/failure state, so a failure in one step (e.g. log upload) does not prevent the others from completing. On iOS, prefer `syncWithStatus` over the deprecated `sync()` for this reason too. --- ## 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()` (Android) / `syncWithStatus()` (iOS, `sync()` is deprecated) — set clock and read battery 5. `setWifiConfig()` — if bridge, configure WiFi ### Regular Operations 1. `lock()` / `unlock()` — control the lock 2. `sync()` (Android) / `syncWithStatus()` (iOS) — 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_android_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 | Guest key 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 = "DAX5-XXXX", 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: "DAX5-XXXX", 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 = "IGB4-XXXX", 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: "IGB4-XXXX", 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 = "OE1-XXXX", 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: "OE1-XXXX", 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_android_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_android_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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_firmware Title: Firmware Updates Category: BLE SDK ---------------------------------------- # Firmware Updates (DFU) 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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_android_getting_started Title: Getting Started Category: BLE SDK ---------------------------------------- # Getting Started 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") } ``` --- ## 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: "2.0.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 => '2.0.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 = "IGM4-XXXX", 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 = "IGM4-XXXX", 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("IGM4-XXXX") ``` ```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("IGM4-XXXX", key: guestKey) print("Locked") } catch IgloohomeError.timeout { print("Lock not in range") } catch { print("Error: \(error.localizedDescription)") } // 4. Sync device state — prefer syncWithStatus (sync() is deprecated) for await status in sdk.syncWithStatus( "IGM4-XXXX", key: guestKey, getDeviceToken: accessToken, storeLogsToken: accessToken, updateDeviceToken: accessToken ) { if status.operation == .GET_BATTERY_LEVEL, status.isSuccess { print("Battery synced") } } // 5. Disconnect when done try? await sdk.disconnect("IGM4-XXXX") ``` --- ## 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_android_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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_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. `"EK1-XXXX"`). | | `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. `"IGM4-XXXX"`). | | `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 = "EK1-XXXX", accessoryGuestKey = ekeyWithAddLock, lockDeviceId = "IGM4-XXXX", 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: "EK1-XXXX", accessoryGuestKey: ekeyWithAddLock, lockBluetoothDeviceName: "IGM4-XXXX", 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. `"IGM4-XXXX"`). | | `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 = "EK1-XXXX", accessoryGuestKey = ekeyWithDeleteLock, lockDeviceId = "IGM4-XXXX", 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: "EK1-XXXX", accessoryGuestKey: ekeyWithDeleteLock, lockBluetoothDeviceName: "IGM4-XXXX", 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_android_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. `"IGM4-XXXX"`). | | `key` | `String` | Yes | Guest key 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_android_errors). ### Example ```kotlin try { sdk.lock( deviceId = "IGM4-XXXX", 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("IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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("IGM4-XXXX", 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("IGM4-XXXX") ``` ```swift try? await sdk.disconnect("IGM4-XXXX") ``` --- ## 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_android_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. `"SP2Xo01231"`). | | `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 = "SP2Xo01231", 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: "SP2Xo01231", 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 = "SP2Xo01231", 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: "SP2Xo01231", 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. `"EB1-XXXX"`). | | `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 = "EB1-XXXX", 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: "EB1-XXXX", 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_android_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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_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_android_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 @available(*, deprecated, message: "Use `syncWithStatus` instead") 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 | Guest key 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 = "IGM4-XXXX", 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 // Deprecated on iOS — prefer syncWithStatus below. do { let result = try await sdk.sync( "IGM4-XXXX", 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. --- ## syncWithStatus Syncs the lock via Bluetooth and emits granular status updates for each sync operation. Useful for showing per-step progress in the UI. ### Signature ```kotlin fun syncWithStatus( deviceId: String, key: String, timeInSeconds: Long? = null, getDeviceToken: String, storeLogsToken: String, updateDeviceToken: String, operationId: Int? = null, ): Flow ``` ```swift func syncWithStatus( _ deviceId: String, key: String, getDeviceToken deviceToken: String, storeLogsToken storeToken: String, updateDeviceToken updateToken: String, timeInSeconds time: Int? = nil ) -> AsyncStream ``` ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `deviceId` | `String` | Yes | Bluetooth device name of the lock. | | `key` | `String` | Yes | Guest key 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 SyncResultStatus( val operation: Operation, val isSuccess: Boolean, val error: IglooHomeException?, ) { enum class Operation { SET_TIME, GET_BATTERY_LEVEL, SYNC_ACTIVITY_LOGS, } } ``` ```swift public struct SyncResultStatus: Sendable { public enum Operation: Sendable { case SET_TIME case GET_BATTERY_LEVEL case SYNC_ACTIVITY_LOGS } public let operation: SyncResultStatus.Operation public let isSuccess: Bool public let error: Error? } ``` | Field | Description | |-------|-------------| | `operation` | The sync sub-operation that completed. | | `isSuccess` | `true` if the operation succeeded. | | `error` | The exception if the operation failed, `null` on success. | ### Example ```kotlin try { sdk.syncWithStatus( deviceId = "IGM4-XXXX", key = guestKey, getDeviceToken = accessToken, storeLogsToken = accessToken, updateDeviceToken = accessToken, ).collect { status -> val label = when (status.operation) { SyncResultStatus.Operation.SET_TIME -> "Set time" SyncResultStatus.Operation.GET_BATTERY_LEVEL -> "Battery level" SyncResultStatus.Operation.SYNC_ACTIVITY_LOGS -> "Activity logs" } if (status.isSuccess) { println("$label: OK") } else { println("$label: FAILED — ${status.error?.message}") } } } catch (e: IglooHomeException.ConnectionException) { showError("unexpected bluetooth connection issue") } catch (e: IglooHomeException.BluetoothException) { showError("Please enable Bluetooth") } catch (e: IglooHomeException) { showError("Sync failed: ${e.message}") } ``` ```swift for await status in sdk.syncWithStatus( "IGM4-XXXX", key: guestKey, getDeviceToken: accessToken, storeLogsToken: accessToken, updateDeviceToken: accessToken ) { let label: String switch status.operation { case .SET_TIME: label = "Set time" case .GET_BATTERY_LEVEL: label = "Battery level" case .SYNC_ACTIVITY_LOGS: label = "Activity logs" } if status.isSuccess { print("\(label): OK") } else { print("\(label): FAILED — \(status.error?.localizedDescription ?? "unknown")") } } ``` ### Notes - Each `SyncResultStatus` emission represents one completed sub-operation. - Unlike `sync()`, individual operation failures do not throw — they are reported via `isSuccess = false` and `error`. - The flow emits one status per operation in order: `SET_TIME`, `GET_BATTERY_LEVEL`, `SYNC_ACTIVITY_LOGS`. - Use this method when you need to show per-step progress or handle partial sync failures gracefully. - iOS doc comment notes: the stream finishes on the first failure, and battery level is not pushed to the server for Switch-type devices. ### FILE: works/sdk_android_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_android_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? = 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 | Guest key 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 = "DAX5-XXXX", 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: "DAX5-XXXX", 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 = "IGB4-XXXX", 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: "IGB4-XXXX", 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 = "OE1-XXXX", 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: "OE1-XXXX", 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_android_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_android_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? = 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_firmware Title: Firmware Updates Category: BLE SDK ---------------------------------------- # Firmware Updates (DFU) 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? = 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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_android_getting_started Title: Getting Started Category: BLE SDK ---------------------------------------- # Getting Started 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") } ``` --- ## 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 Igloohome 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 = "IGM4-XXXX", 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 = "IGM4-XXXX", 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("IGM4-XXXX") ``` ```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("IGM4-XXXX", 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("IGM4-XXXX", 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("IGM4-XXXX") ``` --- ## 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_android_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? = 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_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. `"EK1-XXXX"`). | | `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 = "EK1-XXXX", accessoryGuestKey = ekeyWithAddLock, lockDeviceId = "IGM4-XXXX", ) 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: "EK1-XXXX", accessoryGuestKey: ekeyWithAddLock, lockBluetoothDeviceName: "IGM4-XXXX") 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 = "EK1-XXXX", accessoryGuestKey = ekeyWithDeleteLock, lockDeviceId = "IGM4-XXXX", ) 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: "EK1-XXXX", accessoryGuestKey: ekeyWithDeleteLock, lockBluetoothDeviceName: "IGM4-XXXX") 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_android_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. `"IGM4-XXXX"`). | | `key` | `String` | Yes | Guest key 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_android_errors). ### Example ```kotlin try { sdk.lock( deviceId = "IGM4-XXXX", 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("IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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("IGM4-XXXX", 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("IGM4-XXXX") ``` ```swift try? await sdk.disconnect("IGM4-XXXX") ``` --- ## 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_android_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? = 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. `"SP2Xo01231"`). | | `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 = "SP2Xo01231", 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: "SP2Xo01231", 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 = "SP2Xo01231") 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: "SP2Xo01231") 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. `"EB1-XXXX"`). | | `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 = "EB1-XXXX", 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: "EB1-XXXX", 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_android_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? = 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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_android_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 let bluetoothDeviceName: String public let isActive: Bool public let isPaired: Bool public let isG3: Bool } ``` | Field | Description | |-------|-------------| | `deviceId` | Bluetooth device name (e.g. `"IGM4-XXXX"`, `"SP2Xo01231"`). | | `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_android_sync Title: Sync & Jobs Category: BLE SDK ---------------------------------------- # Sync & Jobs 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 | Guest key 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 = "IGM4-XXXX", 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( "IGM4-XXXX", 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 | Guest key 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 = "IGM4-XXXX", 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: "IGM4-XXXX", 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. ## Documentation ### FILE: home Title: Overview Category: Documentation ---------------------------------------- # igloohome Documentation Welcome to the igloohome API documentation. igloohome is a revolutionary smart access solution that supports both online and offline access needs. Our patented AlgoPIN technology allows you to grant access without WiFi or cellular data. This API exposes a number of functionalities supported on the igloohome app, such as: - AlgoPIN codes - Remote control via a Bridge - Notifications via Webhooks ## HTTP Request Header Format | Key | Value | | --- | --- | | Authorization | `Bearer {access_token}` | | Content-Type | `application/json` | See [Getting Started with igloohome API](/home/getting-started-igloohome-api) (build for yourself) or [Getting Started with iglooconnect](/home/getting-started-iglooconnect) (build for others) 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: | | Build for yourself | Build for others | | --- | --- | --- | | Flow | OAuth 2.0 Client Credentials | OAuth 2.0 Authorization Code | | Who authenticates | Your own account | Each end user, individually | | Guide | [igloohome API](/home/getting-started-igloohome-api) | [iglooconnect](/home/getting-started-iglooconnect) | ### Your server to our server Server-to-server integrations (no end user login) use the OAuth 2.0 Client Credentials flow via **igloohome API** — build for yourself. Your backend authenticates directly with its own Client ID and Secret. See [Getting Started with igloohome API](/home/getting-started-igloohome-api). ```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 remote 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 Welcome to the iglooworks API documentation. iglooworks is a revolutionary smart access solution that supports both online and offline access needs. Our patented algoPIN technology allows you to grant access without WiFi or cellular data. This API exposes a number of functionalities supported on the iglooworks app, such as: - algoPIN codes - Remote control via a Bridge - Notifications via Webhooks ## HTTP Request Header Format | Key | Value | | --- | --- | | Authorization | `Bearer {access_token}` | | Content-Type | `application/json` | See [Getting Started with client credentials iglooworks API](/works/getting-started-client-credentials) (build for yourself) or [Getting Started with code flow iglooworks API](/works/getting-started-code-flow) (build for others) 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: | | Build for yourself | Build for others | | --- | --- | --- | | 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/getting-started-client-credentials) | [Code flow](/works/getting-started-code-flow) | ### Your server to our server Server-to-server integrations (no end user login) use the OAuth 2.0 Client Credentials flow via **iglooworks API** — build for yourself. Your backend authenticates directly with its own Client ID and Secret. See [Getting Started with iglooworks API](/works/getting-started-client-credentials). ```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 remote 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/getting-started-iglooconnect Title: iglooconnect Category: Getting Started ---------------------------------------- # Getting Started with iglooconnect *Build for others.* 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 [Getting Started with igloohome API](/home/getting-started-igloohome-api) — *build for yourself*. ## Prerequisites Before starting, ensure you have: - A valid igloohome business partnership - Your callback URL(s) ready ## Authentication Setup ### 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 | Config device setting | | 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 | | openid | OpenID Connect authentication | | profile | User profile information | ## Implementation Guide ### 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}&state={STATE} ``` #### 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 | | state | No | Recommended. Used for security and state preservation. | #### State Parameter Usage When your app adds a `state` parameter to a request, the server returns its value to your app when redirecting your user. Common use cases: - **Security:** Guard against Cross-Site Request Forgery (CSRF) attacks. - **Context/Identification:** Identify your customer or maintain session state. You can include any data your integration requires to recognize the user or context upon their return. Note: You can't set the value of a state parameter to a URL-encoded JSON string. To pass a string in that format, base64 encode it, then decode it in your app. 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+openid+profile&state=xyz987" ``` #### Error Responses | Error Code | Description | Solution | | --- | --- | --- | | invalid_request | Missing required parameter (e.g., grant_type) | Check all required parameters are included | | invalid_client | Invalid client ID or secret | Verify credentials with igloohome team | | invalid_scope | Requested scope not available | Use only supported scopes from the list above | | invalid_grant | Authorization code expired or already used | Request new authorization code | ### Successful Authentication After successful authentication, the server redirects to your callback URL with an authorization code. If a `state` parameter was provided in the initial request, it is returned here unchanged. ``` https://{REDIRECT_URI}?code={AUTH_CODE}&state={STATE} ``` #### 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=http%3A%2F%2Flocalhost%3A3000%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 | ### 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. ## 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 ### "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.com ### FILE: home/getting-started-igloohome-api Title: igloohome API Category: Getting Started ---------------------------------------- # Getting Started with igloohome API *Build for yourself.* This guide will help 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 [Getting Started with iglooconnect](/home/getting-started-iglooconnect) — *build for others*. ## 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 refreshing it 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. Token Refresh (24h later) App->>Auth: POST /oauth2/token (refresh) 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://access.igloocompany.co/api-access) - 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 | Update device setting | | 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 | #### 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" | ### Using Your Token Include your access token in API requests: ```bash curl --request GET \ --url https://api.igloodeveloper.co/igloohome/devices \ --header 'Authorization: Bearer {your_access_token}' ``` ## Token Management ### Token Expiration Access tokens expire after 24 hours and must be refreshed daily. ### Best Practices - Store tokens securely - Calculate expiry time: `current_time + expires_in` - Implement automatic token refresh before expiration ## 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 | ### Troubleshooting **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 ## Usage Examples ### 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/get-devices igloohomeapi/lock-bridge-proxied-job igloohomeapi/unlock-bridge-proxied-job' ``` ### FILE: home/supported-devices Title: Supported Devices Category: Getting Started ---------------------------------------- # Supported Devices on igloohome API V2 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 [What is algoPIN?](/home/what-is-algopin). *Last updated: July 16, 2025* ### 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 | | EK1X | Keypad | Yes | | IEF1 | Key Fob | No | ### FILE: home/what-is-algopin Title: What is algoPIN? Category: Getting Started ---------------------------------------- # What is algoPIN? algoPIN is a feature that allows you to generate valid PIN codes even when you don't have WiFi, cellular, or remote access to your lock. ## Types of algoPIN Codes - One-Time (OTP) - Permanent - Duration (Hourly) — 1 to 672 hours - Duration (Daily) — 29 to 367 days ## algoPIN Limitations | Limitation | Explanation | | --- | --- | | Only on algoPIN-supported devices | The device has to support algoPIN. See [Supported Devices](/home/supported-devices). | | Paired devices | The device has to be paired, and it has to be paired using the igloohome app. | | 24-hour activation | algoPIN codes that last more than 24 hours need to be activated on the lock. To activate it, use the PIN code within the first 24 hours of its validity. | | 256 rule | 256-hour algoPIN deletion limitation. If an algoPIN is created with a start date more than 256 hours from the current time, it can't be deleted. | | 199 rule | The whitelist and blacklist of the lock is 199 PIN codes max (both algoPIN and Bluetooth PIN codes). | | Number of PINs per duration | For a specified duration, you may only generate a fixed number of PINs, indicated by the number of variances. | ## Variance: Multiple algoPIN Codes for the Same Duration For a given duration, you can create multiple unique algoPIN codes. | PIN Code Type | Number of Variances | | --- | --- | | OTP | 5 | | Permanent | 5 | | Duration (Hourly) | 3 | | Duration (Daily) | 3 | Example: the following request returns a unique PIN code (e.g. `4642226`). ```bash curl --request POST \ --url https://api.igloodeveloper.co/igloohome/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.igloodeveloper.co/igloohome/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" }' ``` ## Activation Limitation algoPIN codes that last longer than 24 hours require activation on the lock. To activate one, use the algoPIN code on the lock within the 24-hour window. ### FILE: works/getting-started-client-credentials Title: Client Credentials Category: Getting Started ---------------------------------------- # Getting Started ## 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. Token Refresh (24h later) App->>Auth: POST /oauth2/token (refresh) 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/edit-pin-bridge-proxied-job` | Edit custom PINs via bridge | | `iglooworksapi/delete-pin-job` | Create a job to delete a PIN | | `iglooworksapi/get-devices` | Retrieve device list | | `iglooworksapi/get-job-status` | Get the status of a specific job | | `iglooworksapi/get-master-pin` | Get the master PIN of a device | | `iglooworksapi/get-properties` | Retrieve property list | | `iglooworksapi/get-access` | Retrieve access list for a property | | `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-duration` | Create duration-based AlgoPINs | | `iglooworksapi/algopin-otp` | Create one-time access AlgoPINs (OTP) | | `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 clientId := "your_client_id_here" clientSecret := "your_client_secret_here" encodedCredentials := base64.StdEncoding.EncodeToString([]byte(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 clientId := "your_client_id_here" clientSecret := "your_client_secret_here" encodedCredentials := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) const params = new URLSearchParams({ grant_type: 'client_credentials', scope: 'iglooworksapi/algopin-onetime iglooworks/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" | ### Using Your Token Include your access token in API requests: ```bash curl --request GET \ --url https://api.iglooworks.co/v2/devices \ --header 'Authorization: Bearer {your_access_token}' ``` ## Token Management ### Token Expiration Access tokens expire after 24 hours and must be refreshed daily. ### Best Practices - Store tokens securely - Calculate expiry time: `current_time + expires_in` - Implement automatic token refresh before expiration ## 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 iglooaccess dashboard | | `invalid_scope` | Requested scope doesn't exist | Check available scopes list | ### Troubleshooting **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 ## Usage Examples ### 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/get-devices iglooworksapi/lock-bridge-proxied-job iglooworksapi/unlock-bridge-proxied-job' ``` ### FILE: works/getting-started-code-flow Title: Code Flow Category: Getting Started ---------------------------------------- # Getting Started with iglooworks code flow integration ## 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 Setup ### 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 igloohome 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/edit-pin-bridge-proxied-job` | Edit custom PINs via bridge | | `iglooworksapi/delete-pin-job` | Create a job to delete a PIN | | `iglooworksapi/get-devices` | Retrieve device list | | `iglooworksapi/get-job-status` | Get the status of a specific job | | `iglooworksapi/get-master-pin` | Get the master PIN of a device | | `iglooworksapi/get-properties` | Retrieve property list | | `iglooworksapi/get-access` | Retrieve access list for a property | | `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-duration` | Create duration-based AlgoPINs | | `iglooworksapi/algopin-otp` | Create one-time access AlgoPINs (OTP) | | `iglooworksapi/get-departments` | Retrieve department list | | `iglooworksapi/get-account-detail` | Get account detail | ## Implementation Guide ### 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 required parameter (e.g., `grant_type`) | Check all required parameters are included | | `invalid_client` | Invalid client ID or secret | Verify credentials with iglooworks team | | `invalid_scope` | Requested scope not available | Use only supported scopes from the list above | | `invalid_grant` | Authorization code expired or already used | Request new authorization code | ### 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 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' \ --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 | ### 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 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' \ --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. ## 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 #### "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.com](mailto:dev+support@igloohome.com) ### FILE: works/supported-devices Title: Supported Devices Category: Getting Started ---------------------------------------- # Supported Devices on iglooworks API V2 ## 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:** July 16, 2025 #### algoPIN Support Devices marked with algoPIN support can generate time-sensitive PIN codes for temporary access. #### 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 | | 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 | | EK1X | Keypad | Yes | | IEF1 | Key Fob | No | ### FILE: works/what-is-algopin Title: What is AlgoPIN Category: Getting Started ---------------------------------------- # What is algoPIN? ## Overview algoPIN is a feature that allows you to **generate valid PIN codes** even when you don't have WiFi, cellular, or remote access to your lock. ## Sequence diagram ```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 1. One-Time (OTP) 2. Permanent 3. Duration (Hourly) - 1 to 672 hours 4. Duration (Daily) - 29 to 367 days ## algoPIN limitations | Limitation | Explanation | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Only on algoPIN-supported devices | The device has to support algoPIN. See list [here](https://igloohome.stoplight.io/docs/iglooworks/osqtc5xu25qd5-supported-devices-on-iglooworks-api-v2). | | Paired devices | The device has to be paired, and it has to be paired using the iglooworksapp. | | 24-hour activation | algoPIN codes that last more than 24 hours need to be activated on the lock. To activate it, simply use the PIN code within the first 24 hours of its validity. | | 256 rule | 256-hour algoPIN deletion limitation. This means that if algoPIN created with start date > 256 hours from current time, they can't delete the PIN. | | 199 rule | The whitelist and blacklist of the lock is 199 PIN codes max (both algoPIN and Bluetooth PIN codes). | | Number of PINs per duration | For a specified duration, you may only generate a fixed number of PINs indicated by the [number of variances](#generate-multiple-algopin-codes-for-the-same-duration-variance). | ## Variance feature: Multiple algoPIN codes for the same duration **For a given duration**, you can create a **multiple unique algoPIN codes**. | PIN code Type | Number of Variances | | ------------------- | --------------------- | | OTP | 5 | | Permanent | 5 | | Duration (Hourly) | 3 | | Duration (Daily) | 3 | Here's an example that illustrates this: | Start time | End time | Duration PIN codes | deviceId | | ------------ | ---------- | --------------------------------- | -------------- | | 2PM | 3PM | 511093918, 945844647, 052368115 | OE1X02abc123 | | 2PM | 4PM | 633654730, 437755350, 337288611 | OE1X02abc123 | | 2PM | 5PM | 381750736, 830094870, 719038999 | OE1X02abc123 | | 3PM | 4PM | 752994740, 538785775, 856468831 | OE1X02abc123 | | 3PM | 5PM | 297248165, 196981787, 384522901 | OE1X02abc123 | | 3PM | 5PM | 948950871, 142404925, 817840781 | IGP114cde456 | For example, the following request would return a unique PIN code (e.g. **'4642226'**). ```curl curl --request POST \ --url https://api.iglooworks.co/v2/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" }' ``` But by changing the variance, you can get a second unique PIN code (e.g. **'1419472'**) for the same duration. ```curl curl --request POST \ --url https://api.iglooworks.co/v2/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" }' ``` ## Activation limitation For algoPIN codes that last longer than 24 hours, activation on the lock is required. To activate an algoPIN code, simply use the algoPIN code on the lock within the 24-hour window. The following illustrations detail how this 24-hour activation requirement works: ![iiglooworksapp onboarding (5).png](https://stoplight.io/api/v1/projects/cHJqOjEzNjUyMA/images/gX9zSKbWbrA) ![iiglooworksapp onboarding (6).png](https://stoplight.io/api/v1/projects/cHJqOjEzNjUyMA/images/wOZl2UCkZD8) ![iiglooworksapp onboarding (7).png](https://stoplight.io/api/v1/projects/cHJqOjEzNjUyMA/images/SmeHePImJKM)