I would love to be able to make better use of the iiQ APIs.
If any other users would like to share ideas, repos w code, examples, etc, let’s do it in this thread.
Of if you know of other threads, please post them here.
- Home
- Community overview
- Ask the Community
- Other
- API Help Thread - posts about iiQ API use
API Help Thread - posts about iiQ API use
- February 11, 2026
- 12 replies
- 233 views
12 replies
- Community Manager
- February 17, 2026
Hi!
Best,
- Observer
- April 10, 2026
This is 1 of 3 posts to for the Senior Device and Fee solution.
As we prepare for graduation, I want to provide a way for seniors to verify what devices they have assigned to them and their outstanding fees.
This will be done by them submitting a google form to request the information. Their device and fee information will be sent to them in an email.
This is not the complete solution. But these are the building blocks I needed to create it.
The first thing I need to do is create a view of all seniors and then obtain the view id. Here is the function I used to retrieve my user views.
/**
* Queries the IIQ User Views endpoint and parses the 'Items' array
* to find the GUID (ViewId) for your senior list.
*/
function findSeniorViewGuid() {
const bearerToken = getBearerToken();
const siteId = getSiteId();
const url = `${BASE_URL}/users/views`;
const options = {
method: 'get',
headers: {
'Authorization': `Bearer ${bearerToken}`,
'SiteId': siteId,
'Accept': 'application/json'
},
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
// Based on your JSON example, the data is in the 'Items' property
const views = result.Items || [];
console.log("--- YOUR USER VIEWS ---");
if (views.length > 0) {
views.forEach(view => {
// Logging the Name and the ViewId (GUID)
console.log(`Name: ${view.Name}`);
console.log(`GUID: ${view.ViewId}`);
console.log(`Entity: ${view.ViewTypeId}`); // Helps confirm if it's a User view
console.log('---------------------------');
});
} else {
console.warn("No views found in the 'Items' array.");
console.log("Full Response Structure: " + Object.keys(result).join(", "));
}
} catch (e) {
console.error("Script Error: " + e.message);
}
}
- Observer
- April 10, 2026
This is 2 of 3 posts to for the Senior Device and Fee solution.
Due to my "learn as you go" approach to the API, I was not aware of the meta data contained in a user record. I used this function to return the record structure to the execution log.
This allowed me to see where a users assigned devices are contained within that structure.
/**
* Standalone Diagnostic: Fetches the Senior View and logs the RAW data
* structure of the first record found. Use this to verify property names.
*/
function debugFirstAssetStructure() {
const viewId = 'your view id';
const productId = 'IIQ product id';
try {
const bearerToken = getBearerToken();
const siteId = getSiteId();
// We only need the first record for this test, so we set $s=1
const url = `${BASE_URL}/assets?$s=1`;
const payloadData = {
"ProductId": productId,
"Filters": [{ "Facet": "View", "Id": viewId }]
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': {
'Authorization': `Bearer ${bearerToken}`,
'SiteId': siteId
},
'payload': JSON.stringify(payloadData),
'muteHttpExceptions': true
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
const items = result.Items || [];
if (items.length > 0) {
const firstAsset = items[0];
// We use JSON.stringify with (null, 2) to make it "pretty print" with indentation
Logger.log("--- RAW ASSET DATA STRUCTURE ---");
Logger.log(JSON.stringify(firstAsset, null, 2));
Logger.log("--- END OF RECORD ---");
} else {
Logger.log("No assets found in the specified view.");
}
} catch (e) {
Logger.log(`❌ Diagnostic Error: ${e.message}`);
}
}
- Observer
- April 10, 2026
This is 3 of 3 posts to for the Senior Device and Fee solution.
I used this function to extract the assigned devices and write them to a sheet.
As always, please verify and use good judgement when it comes to the information I post.
/**
* Refreshes the 'Senior Assigned Devices' sheet from the IIQ View.
* Populates Column A with Email, Column C with SchoolIdNumber, and Column I with Category.
*/
function refreshAssetLookupTable() {
const viewId = 'Your view id';
const sheetName = 'Senior Assigned Devices';
const productId = 'IIQ product ID';
try {
const bearerToken = getBearerToken();
const siteId = getSiteId();
const url = `${BASE_URL}/assets?$s=99999`;
const payloadData = {
"ProductId": productId,
"Filters": [{ "Facet": "View", "Id": viewId }]
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': {
'Authorization': `Bearer ${bearerToken}`,
'SiteId': siteId
},
'payload': JSON.stringify(payloadData),
'muteHttpExceptions': true
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
const items = result.Items || [];
if (items.length > 0) {
const allData = items.map(asset => {
// Mapping based on your specific IIQ response structure
const studentEmail = (asset.Owner?.Email || asset.Owner?.EmailAddress || "").toLowerCase().trim();
const studentId = asset.Owner?.SchoolIdNumber || asset.Owner?.EmployeeNumber || "";
const categoryName = asset.Model?.Category?.Name || "Other";
return [
studentEmail, // Column A: Email
asset.Location?.Name || "District 279", // Column B: Location
studentId, // Column C: School Id #
asset.Model?.Name || "Unknown Device", // Column D: Model
asset.Owner?.FullName || "Student", // Column E: Owner
asset.AssetTag || "N/A", // Column F: Tag
asset.SerialNumber || "N/A", // Column G: Serial
asset.Status?.Name || "Assigned", // Column H: Status
categoryName // Column I: Category
];
});
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
// Clear existing data and set new values for all 9 columns (A through I)
if (sheet.getLastRow() > 1) {
sheet.getRange(2, 1, sheet.getLastRow() - 1, 9).clearContent();
}
sheet.getRange(2, 1, allData.length, 9).setValues(allData);
// Update the sync timestamp on the Fees sheet
const feeSheet = ss.getSheetByName("Senior Fees");
const formattedDate = Utilities.formatDate(new Date(), "GMT-5", "MMMM d, yyyy 'at' h:mm a");
if (feeSheet) feeSheet.getRange("N2").setValue(formattedDate);
Logger.log(`✅ Successfully synced ${allData.length} devices.`);
}
} catch (e) {
Logger.log(`❌ Sync Error: ${e.message}`);
}
}
- Author
- Mentor
- April 13, 2026
Oh, so is the UUID still not exportable in the GUI View (export sftp) and has to be done with API?
- Observer
- April 13, 2026
Oh, so is the UUID still not exportable in the GUI View (export sftp) and has to be done with API?
Good question. I don’t have that answer. I haven’t had a reason to export a view in that manner (sftp).
- Author
- Mentor
- April 13, 2026
Yes, I just confirmed. The iiQ Asset ID (UUID that can be used) is already available in the Asset Views. We use the scheduled sftp View, ingest into a database, and use for various purposes.
- Observer
- April 13, 2026
Yes, I just confirmed. The iiQ Asset ID (UUID that can be used) is already available in the Asset Views. We use the scheduled sftp View, ingest into a database, and use for various purposes.
I totally misunderstood your question. I thought you were asking about the GUID for the actual view. So theoretically, you could have that database connect directly to your view via the API and pull in the same data.
- Author
- Mentor
- May 6, 2026
Have any of you used OnlySetMappedProperties in the header? I’ve not tried, and I have use for it. Just curious.
The key to keeping all other data unchanged is the ApiFlags: OnlySetMappedProperties header.
- Author
- Mentor
- May 6, 2026
And on this topic, it seems like this DOES NOT WORK for custom fields. Has anybody experienced anything different?
- Author
- Mentor
- May 6, 2026
And on this topic, it seems like this DOES NOT WORK for custom fields. Has anybody experienced anything different?
I may have been given some incomplete information from a peer. Testing with a different API endpoint now. Seems like the updated endpoint may allow us to do what we want.
- Observer
- May 7, 2026
I would love to be able to make better use of the iiQ APIs.
If any other users would like to share ideas, repos w code, examples, etc, let’s do it in this thread.
Of if you know of other threads, please post them here.
I’ve made a couple of API tools for help desk, here’s something from a couple months ago for summer device inventories.
import requests
import toml
import sys
def main():
try:
config = toml.load("config.toml")
API_BASE = config["API_URL"]
API_TOKEN = config["API_TOKEN"]
DATE_OF_INVENTORY = config["DATE_OF_INVENTORY"]
AVAILABLE_STATUS_GUID = config["AVAILABLE_STATUS_GUID"]
INVENTORY_DATE_CUSTOM_FIELD_ID = config["INVENTORY_DATE_CUSTOM_FIELD_ID"]
except FileNotFoundError:
print("Error: config.toml not found. Please ensure it exists in the same directory.")
sys.exit(1)
except KeyError as e:
print(f"Error: Missing required config value: {e}")
sys.exit(1)
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Accept": "application/json",
}
while True:
asset_tag = input("\nEnter asset tag: ").strip()
if not asset_tag:
print("No asset tag entered.")
continue
try:
get_resp = requests.get(f"{API_BASE}/assets/assettag/{asset_tag}", headers=headers)
get_resp.raise_for_status()
data = get_resp.json()
except requests.exceptions.ConnectionError:
print("Error: Could not connect to the API. Check your network connection and API_URL.")
again = input("\nProcess another device? (y/n): ").strip().lower()
if again != "y":
print("Exiting.")
break
continue
except requests.exceptions.HTTPError as e:
print(f"Error fetching asset: {e}")
again = input("\nProcess another device? (y/n): ").strip().lower()
if again != "y":
print("Exiting.")
break
continue
if not data.get("Items"):
print(f"No asset found for tag: {asset_tag}")
else:
asset = data["Items"][0]
asset_id = asset["AssetId"]
location_id = asset.get("LocationId")
location_name = asset.get("Location", {}).get("Name", "Unknown")
current_owner = asset['Owner']['FullName'] if asset.get('Owner') else 'None'
current_status = asset['Status']['Name'] if asset.get('Status') else 'Unknown'
current_room = asset.get('LocationRoom', {}).get('Name', 'None') if asset.get('LocationRoom') else 'None'
current_date = 'N/A'
for field in asset.get("CustomFieldValues", []):
if field.get("CustomFieldTypeId") == INVENTORY_DATE_CUSTOM_FIELD_ID:
current_date = field.get("Value", "N/A")
break
selected_room = None
try:
rooms_resp = requests.get(
f"{API_BASE}/locations/rooms/{location_id}/search?$s=100", # Hardcapping this at 100 since this is IIQ's maximum.
headers=headers
)
rooms_resp.raise_for_status()
rooms = rooms_resp.json().get("Items", [])
if rooms:
print(f"\n{location_name} Rooms:")
for i, room in enumerate(rooms, 1):
print(f" {i:>3}. {room['Name']}")
while True:
room_input = input("\nSelect room number (or 0 to skip): ").strip()
if room_input == "0":
break
if room_input.isdigit() and 1 <= int(room_input) <= len(rooms):
selected_room = rooms[int(room_input) - 1]
break
print(f"Invalid selection. Enter a number between 1 and {len(rooms)}, or 0 to skip.")
else:
print("No rooms found for this building.")
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as e:
print(f"Warning: Could not fetch rooms: {e}. Room will not be updated.")
new_room_name = selected_room['Name'] if selected_room else 'Unchanged'
print(f"\nFound: {asset['Name']} | Serial: {asset['SerialNumber']}")
print(f"\n{'Field':<22} {'Before':<30} {'After':<30}")
print("-" * 82)
print(f"{'Owner':<22} {current_owner:<30} {'None':<30}")
print(f"{'Last Inventory Date':<22} {current_date:<30} {DATE_OF_INVENTORY:<30}")
print(f"{'Status':<22} {current_status:<30} {'Available':<30}")
print(f"{'Room':<22} {current_room:<30} {new_room_name:<30}")
confirm = input("\nProceed with these changes? (y/n): ").strip().lower()
if confirm != "y":
print("Skipped.")
else:
payload = asset
payload["OwnerId"] = None
payload["Owner"] = None
payload["LastInventoryDate"] = DATE_OF_INVENTORY
payload["StatusTypeId"] = AVAILABLE_STATUS_GUID
payload["AssetStatusTypeId"] = AVAILABLE_STATUS_GUID
payload["Status"] = {
"AssetStatusTypeId": AVAILABLE_STATUS_GUID,
"Scope": "Site",
"Name": "Available",
"IsRetired": False,
"SupportsNSAs": False
}
if selected_room:
payload["LocationRoomId"] = selected_room["LocationRoomId"]
payload["LocationRoom"] = selected_room
field_found = False
for field in payload.get("CustomFieldValues", []):
if field.get("CustomFieldTypeId") == INVENTORY_DATE_CUSTOM_FIELD_ID:
field["Value"] = DATE_OF_INVENTORY
field_found = True
break
if not field_found:
payload["CustomFieldValues"].append({
"CustomFieldTypeId": INVENTORY_DATE_CUSTOM_FIELD_ID,
"Value": DATE_OF_INVENTORY
})
payload["UpdateCustomFields"] = True
try:
post_resp = requests.post(f"{API_BASE}/assets/{asset_id}", headers=headers, json=payload)
post_resp.raise_for_status()
print(f"Success! Asset {asset_tag} updated. (Status {post_resp.status_code})")
except requests.exceptions.HTTPError as e:
print(f"Error: Failed to update asset {asset_tag}. {e}")
except requests.exceptions.ConnectionError:
print("Error: Lost connection while saving. Changes were not applied.")
again = input("\nProcess another device? (y/n): ").strip().lower()
if again != "y":
print("Exiting.")
break
main()
The main thing with custom fields and GUIDs is that I’ve had better luck just exporting a HAR file from my browser and watching whatever endpoints show up as I click the buttons to make requests appear. The documentation is pretty weak compared to other APIs I’ve used in the past (MS Graph / Discord etc.).
Enter your E-mail address. We'll send you an e-mail with instructions to reset your password.
Scanning file for viruses.
Sorry, we're still checking this file's contents to make sure it's safe to download. Please try again in a few minutes.
OKThis file cannot be downloaded
Sorry, our virus scanner detected that this file isn't safe to download.
OKIncident IQ is a service management platform built for K-12 school districts, featuring asset management, help ticketing, facilities maintenance solutions, and more.
Corporate Headquarters:
750 Glenwood Ave SE, Suite 320
Atlanta, GA 30316

